mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-07-12 00:32:20 +08:00
Compare commits
30 Commits
01f3a42613
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86e1e6050a | ||
|
|
956634d8ab | ||
|
|
8a1d33bd2b | ||
|
|
aef2c4986d | ||
|
|
b35aa35b1a | ||
|
|
0ef0a83685 | ||
|
|
586f4612ed | ||
|
|
f0183370be | ||
|
|
f4fe727a0e | ||
|
|
d61b27af8f | ||
|
|
8e4300a7ea | ||
|
|
8a666a56b5 | ||
|
|
f4fcbf30ed | ||
|
|
db46013ee0 | ||
|
|
0b532ee191 | ||
|
|
95c253b65a | ||
|
|
c4ba03b289 | ||
|
|
2effc61292 | ||
|
|
54e84fbaac | ||
|
|
41b3ea2c7f | ||
|
|
4836f332f0 | ||
|
|
da49d06209 | ||
|
|
6688655370 | ||
|
|
955e3819b5 | ||
|
|
591990a040 | ||
|
|
0fc7f1d36e | ||
|
|
15155bd8df | ||
|
|
816589ae22 | ||
|
|
d14533d966 | ||
|
|
697f4188c2 |
@@ -11,16 +11,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checking out branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
shell: bash
|
||||
run: curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install android NDK
|
||||
id: setup-ndk
|
||||
uses: https://github.com/nttld/setup-ndk@v1
|
||||
@@ -31,12 +28,6 @@ jobs:
|
||||
run: |
|
||||
mkdir out
|
||||
|
||||
- name: Build webui
|
||||
run: |
|
||||
cd webui
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Install cargo ndk
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[submodule "assets"]
|
||||
path = assets
|
||||
url = https://git.ethanthesleepy.one/ethanaobrien/sif2-runtime-files
|
||||
[submodule "webui"]
|
||||
path = webui
|
||||
url = https://git.ethanthesleepy.one/ethanaobrien/ew-webui
|
||||
712
Cargo.lock
generated
712
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@@ -1,15 +1,15 @@
|
||||
[package]
|
||||
name = "ew"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.12"
|
||||
rusqlite = { version = "0.37.0", features = ["bundled"] }
|
||||
clap = { version = "4.5.37", features = ["derive"]}
|
||||
actix-web = "4.13"
|
||||
rusqlite = { version = "0.38.0", features = ["bundled"] }
|
||||
clap = { version = "4.5.59", features = ["derive"]}
|
||||
base64 = "0.22.1"
|
||||
json = "0.12.4"
|
||||
rand = "0.9.2"
|
||||
rand = "0.10.0"
|
||||
lazy_static = "1.5.0"
|
||||
hex = "0.4.3"
|
||||
hmac = "0.12.1"
|
||||
@@ -17,8 +17,8 @@ md5 = "0.8.0"
|
||||
urlencoding = "2.1.3"
|
||||
sha1 = "0.10.6"
|
||||
substring = "1.4.5"
|
||||
uuid = { version = "1.18.1", features = ["v7"] }
|
||||
rsa = { version = "0.9.9", features = ["sha1"] }
|
||||
uuid = { version = "1.21.0", features = ["v7"] }
|
||||
rsa = { version = "0.9.10", features = ["sha1"] }
|
||||
mime = "0.3.17"
|
||||
sha2 = "0.10.9"
|
||||
include-flate-codegen = "0.3.1"
|
||||
@@ -26,21 +26,23 @@ libflate = "2.2.1"
|
||||
cbc = { version = "0.1.2", features = ["alloc"] }
|
||||
aes = "0.8.4"
|
||||
pem = "3.0.6"
|
||||
ureq = "3.1.4"
|
||||
ureq = "3.2.0"
|
||||
mime_guess = "2.0.5"
|
||||
include_dir = "0.7.4"
|
||||
|
||||
[target.aarch64-linux-android.dependencies]
|
||||
jni = { version = "0.21.1", features = ["invocation", "default"] }
|
||||
jni = { version = "0.21.0", features = ["invocation", "default"], optional = true }
|
||||
|
||||
[target.aarch64-apple-ios.dependencies]
|
||||
objc2 = "0.6.3"
|
||||
objc2-foundation = { version = "0.3.2", features = ["NSFileManager"] }
|
||||
objc2 = { version = "0.6.3", optional = true }
|
||||
objc2-foundation = { version = "0.3.2", features = ["NSFileManager"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
cc = "1.2"
|
||||
|
||||
# To enable this library you MUST comment out lib block below and add --features library
|
||||
[features]
|
||||
library = []
|
||||
library = ["jni", "objc2", "objc2-foundation"]
|
||||
|
||||
#[lib]
|
||||
#crate-type = ["cdylib"]
|
||||
|
||||
1
assets
Submodule
1
assets
Submodule
Submodule assets added at dbb068e72c
4
build.rs
4
build.rs
@@ -9,4 +9,8 @@ fn main() {
|
||||
.compile("libc_code.a");
|
||||
println!("cargo:rerun-if-changed=src/log.c");
|
||||
}
|
||||
|
||||
if !std::fs::exists("webui/index.html").unwrap_or(false) {
|
||||
panic!("Could not compile crate! Missing webui! Did you pull submodules?");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,12 @@ FROM docker.io/library/debian:latest AS builder
|
||||
# First - build
|
||||
|
||||
RUN apt update && apt install -y curl libssl-dev perl git gcc make
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash
|
||||
RUN apt update && apt install -y nodejs
|
||||
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
|
||||
|
||||
WORKDIR /ew/
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
WORKDIR /ew/webui/
|
||||
|
||||
RUN npm i && npm run build
|
||||
|
||||
WORKDIR /ew/
|
||||
|
||||
RUN . "$HOME/.cargo/env" && cargo build --release
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
sif2-ew:
|
||||
image: sif2-ew:latest
|
||||
@@ -14,6 +13,7 @@ services:
|
||||
HIDDEN: false # Will disable the webui
|
||||
DISABLE_IMPORTS: false # Will disable account imports
|
||||
DISABLE_EXPORTS: false # Will disable account exports
|
||||
#IMAGE_ASSET_PATH: /images/ # Images for cards in webui (will default to the public server)
|
||||
|
||||
# Everything below is for the "Help" page
|
||||
#ANDROID_GLOBAL: "link.to/patched/android/global.apk"
|
||||
@@ -25,4 +25,7 @@ services:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./data:/data
|
||||
# You can download from https://git.ethanthesleepy.one/ethanaobrien/ew-webui/releases/download/images/card-thumbnails.zip
|
||||
# No, you should not have a nested folder EXAMPLE: `images/FILENAME.webp`
|
||||
#- ./images:/images
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -25,4 +25,6 @@ asset_android_en=$([ "$EN_ANDROID_ASSET_HASH" != "" ] && echo "--en-android-asse
|
||||
|
||||
asset_ios_en=$([ "$EN_IOS_ASSET_HASH" != "" ] && echo "--en-ios-asset-hash $EN_IOS_ASSET_HASH" || echo "")
|
||||
|
||||
/root/ew/ew --path $directory --port $port --npps4 $npps4 $asset_android_jp $asset_ios_jp $asset_android_en $asset_ios_en $exports $imports $purge $hidden $https --global-android "$ANDROID_GLOBAL" --japan-android "$ANDROID_JAPAN" --global-ios "$IOS_GLOBAL" --japan-ios "$IOS_JAPAN" --assets-url "$ASSET_URL" --max-time $maxTime
|
||||
image_asset_path=$([ "$IMAGE_ASSET_PATH" != "" ] && echo "--image-asset-path $IMAGE_ASSET_PATH" || echo "")
|
||||
|
||||
/root/ew/ew --path $directory --port $port --npps4 $npps4 $asset_android_jp $asset_ios_jp $asset_android_en $asset_ios_en $exports $imports $purge $hidden $https $image_asset_path --global-android "$ANDROID_GLOBAL" --japan-android "$ANDROID_JAPAN" --global-ios "$IOS_GLOBAL" --japan-ios "$IOS_JAPAN" --assets-url "$ASSET_URL" --max-time $maxTime
|
||||
|
||||
24
readme.md
24
readme.md
@@ -5,27 +5,15 @@ A (mostly functioning) server for Love Live! School idol festival 2 MIRACLE LIVE
|
||||
|
||||
## Building
|
||||
|
||||
### Linux
|
||||
|
||||
#### Requirements
|
||||
- [perl](https://www.perl.org/get.html) (This is normally pre-installed)
|
||||
- [rust](https://www.rust-lang.org/tools/install)
|
||||
- [npm](https://www.npmjs.com/)
|
||||
- The [libssl-dev](https://packages.debian.org/buster/libssl-dev) package. This will vary across distros.
|
||||
|
||||
`apt install -y npm libssl-dev perl`
|
||||
|
||||
### Windows
|
||||
|
||||
#### Requirements
|
||||
- [Strawberry Perl](https://strawberryperl.com/)
|
||||
### Requirements
|
||||
- [rust](https://www.rust-lang.org/tools/install)
|
||||
|
||||
### Packaging/Running
|
||||
|
||||
**Build npm:**
|
||||
`cd webui && npm install && npm run build`
|
||||
|
||||
**Build Rust:**
|
||||
**Build:**
|
||||
Debug: `cargo run`
|
||||
Release: `cargo build --release`
|
||||
|
||||
**Docker:**
|
||||
`docker build --tag ew --file docker/Dockerfile .`
|
||||
Will create docker image with tag "ew"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use aes::cipher::BlockEncryptMut;
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
|
||||
|
||||
@@ -48,9 +48,7 @@ pub async fn run_server(in_thread: bool) -> std::io::Result<()> {
|
||||
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))
|
||||
|
||||
@@ -57,7 +57,10 @@ pub struct Args {
|
||||
pub en_android_asset_hash: String,
|
||||
|
||||
#[arg(long, default_value = "", help = "Asset hash for JP Android client.")]
|
||||
pub jp_android_asset_hash: String
|
||||
pub jp_android_asset_hash: String,
|
||||
|
||||
#[arg(long, default_value = "", help = "Path to image assets.")]
|
||||
pub image_asset_path: String
|
||||
}
|
||||
|
||||
pub fn get_args() -> Args {
|
||||
|
||||
@@ -170,7 +170,7 @@ async fn api_req(req: HttpRequest, body: String) -> HttpResponse {
|
||||
pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
|
||||
let args = crate::get_args();
|
||||
let headers = req.headers();
|
||||
if args.hidden && req.path().starts_with("/api/webui/") {
|
||||
if args.hidden && (req.path().starts_with("/api/webui/") || req.path().starts_with("/live_clear_rate.html")) {
|
||||
return not_found(headers);
|
||||
}
|
||||
if headers.get("aoharu-asset-version").is_none() && req.path().starts_with("/api") && !req.path().starts_with("/api/webui") {
|
||||
@@ -199,6 +199,7 @@ pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
|
||||
"/api/webui/startLoginbonus" => webui::start_loginbonus(req, body),
|
||||
"/api/webui/import" => webui::import(req, body),
|
||||
"/api/webui/set_time" => webui::set_time(req, body),
|
||||
"/api/webui/cheat" => webui::cheat(req, body),
|
||||
_ => api_req(req, body).await
|
||||
}
|
||||
} else {
|
||||
@@ -212,9 +213,14 @@ pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
|
||||
"/v1.0/payment/balance" => gree::balance(req),
|
||||
"/web/announcement" => web::announcement(req),
|
||||
"/api/webui/userInfo" => webui::user(req),
|
||||
"/live_clear_rate.html" => clear_rate::clearrate_html(req).await,
|
||||
"/webui/logout" => webui::logout(req),
|
||||
"/api/webui/export" => webui::export(req),
|
||||
"/api/webui/serverInfo" => webui::server_info(req),
|
||||
"/api/webui/listCards" => webui::get_card_info(req),
|
||||
"/api/webui/listMusic" => webui::get_music_info(req),
|
||||
"/api/webui/listLoginBonus" => webui::list_login_bonus(req),
|
||||
"/api/webui/listItems" => webui::list_items(req),
|
||||
_ => api_req(req, body).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +7,7 @@ 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>;
|
||||
@@ -34,6 +35,7 @@ impl SqlClearRate for SQLite {
|
||||
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 {
|
||||
@@ -150,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 {
|
||||
@@ -236,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)
|
||||
}
|
||||
|
||||
239
src/router/clear_rate_template.html
Normal file
239
src/router/clear_rate_template.html
Normal 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
12742
src/router/databases/json/global/music.json
Normal file
12742
src/router/databases/json/global/music.json
Normal file
File diff suppressed because it is too large
Load Diff
12742
src/router/databases/json/music.json
Normal file
12742
src/router/databases/json/music.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use json::{JsonValue, object, array};
|
||||
use actix_web::HttpRequest;
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
|
||||
use crate::encryption;
|
||||
use crate::include_file;
|
||||
|
||||
@@ -15,7 +15,6 @@ 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! {
|
||||
@@ -231,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"
|
||||
@@ -245,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(),
|
||||
@@ -316,7 +313,7 @@ pub fn migration_code(req: HttpRequest) -> HttpResponse {
|
||||
|
||||
let resp = object!{
|
||||
result: "OK",
|
||||
migration_code: uid_to_code(user["user"]["id"].to_string())
|
||||
migration_code: userdata::user::migration::get_acc_token(user["user"]["id"].as_i64().unwrap())
|
||||
};
|
||||
|
||||
send(req, resp)
|
||||
@@ -336,10 +333,10 @@ pub fn migration_password_register(req: HttpRequest, body: String) -> HttpRespon
|
||||
}
|
||||
|
||||
let user = userdata::get_acc(&uid);
|
||||
let code = uid_to_code(user["user"]["id"].to_string());
|
||||
|
||||
let pass = decrypt_transfer_password(&body["migration_password"].to_string());
|
||||
|
||||
userdata::save_acc_transfer(&code, &pass);
|
||||
userdata::user::migration::save_acc_transfer(user["user"]["id"].as_i64().unwrap(), &pass);
|
||||
|
||||
let resp = object!{
|
||||
result: "OK"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use json::{array, object, JsonValue};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use actix_web::{HttpRequest, http::header::{HeaderMap, HeaderValue}};
|
||||
use crate::encryption;
|
||||
|
||||
@@ -43,78 +43,92 @@ pub fn get_region(headers: &HeaderMap) -> bool {
|
||||
}
|
||||
|
||||
pub fn check_for_region(user: &mut JsonValue, headers: &HeaderMap) {
|
||||
let items = if user["data"]["updated_value_list"]["item_list"].is_empty() {user["data"]["item_list"].clone()} else {user["data"]["updated_value_list"]["item_list"].clone()};
|
||||
let is_jp = get_region(headers);
|
||||
if !is_jp || items.is_empty() {
|
||||
// returns true if jp
|
||||
if !get_region(headers) {
|
||||
return;
|
||||
}
|
||||
let mut id = 0;
|
||||
for (i, data) in items.members().enumerate() {
|
||||
if data["master_item_id"] == 15570008 {
|
||||
id = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if id > 0 {
|
||||
if user["data"]["updated_value_list"]["item_list"].is_empty() {
|
||||
user["data"]["item_list"].array_remove(id - 1);
|
||||
} else {
|
||||
user["data"]["updated_value_list"]["item_list"].array_remove(id - 1);
|
||||
}
|
||||
|
||||
let targets = [
|
||||
15570001, 15570002, 15570003, 15570004,
|
||||
15570005, 15570006, 15570007, 15570008
|
||||
];
|
||||
|
||||
let list = if !user["data"]["updated_value_list"]["item_list"].is_empty() {
|
||||
&mut user["data"]["updated_value_list"]["item_list"]
|
||||
} else {
|
||||
&mut user["data"]["item_list"]
|
||||
};
|
||||
|
||||
let mut to_remove: Vec<usize> = list
|
||||
.members()
|
||||
.enumerate()
|
||||
.filter(|(_, item)| {
|
||||
targets.contains(&item["master_item_id"].as_i64().unwrap_or(0))
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
to_remove.reverse();
|
||||
for i in to_remove {
|
||||
list.array_remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
// true - limit reached
|
||||
// false - all good
|
||||
const GIFT_LIMIT: usize = 100000;
|
||||
const LIMIT_ITEMS: i64 = 200000000;
|
||||
const LIMIT_COINS: i64 = 2000000000;
|
||||
const LIMIT_PRIMOGEMS: i64 = 1000000;
|
||||
pub const GIFT_LIMIT: usize = 100000;
|
||||
pub const LIMIT_ITEMS: i64 = 9999999;
|
||||
pub const LIMIT_COINS: i64 = 999999999;
|
||||
pub const LIMIT_PRIMOGEMS: i64 = 9999999;
|
||||
|
||||
pub fn give_shop(master_item_id: i64, count: i64, user: &mut JsonValue) -> bool {
|
||||
let mut has = false;
|
||||
for dataa in user["shop_list"].members_mut() {
|
||||
if dataa["master_shop_item_id"].as_i64().unwrap() == master_item_id {
|
||||
has = true;
|
||||
let new_amount = dataa["count"].as_i64().unwrap() + count;
|
||||
if new_amount > LIMIT_ITEMS {
|
||||
fn give(array: &mut JsonValue, shop_id: &str, master_item_id: i64, limit: i64, count_id: &str, count: i64, default_push: JsonValue) -> bool {
|
||||
for data in array.members_mut() {
|
||||
if data[shop_id].as_i64().unwrap() == master_item_id {
|
||||
if data[count_id].as_i64().unwrap() >= limit {
|
||||
return true;
|
||||
}
|
||||
dataa["count"] = new_amount.into();
|
||||
break;
|
||||
let mut new_amount = data[count_id].as_i64().unwrap() + count;
|
||||
if new_amount > limit {
|
||||
new_amount = limit;
|
||||
}
|
||||
data[count_id] = new_amount.into();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if !has {
|
||||
user["shop_list"].push(object!{
|
||||
master_shop_item_id: master_item_id,
|
||||
count: count
|
||||
}).unwrap();
|
||||
}
|
||||
array.push(default_push).unwrap();
|
||||
false
|
||||
}
|
||||
|
||||
pub fn give_item(master_item_id: i64, amount: i64, user: &mut JsonValue) -> bool {
|
||||
let mut has = false;
|
||||
for dataa in user["item_list"].members_mut() {
|
||||
if dataa["master_item_id"].as_i64().unwrap() == master_item_id {
|
||||
has = true;
|
||||
let new_amount = dataa["amount"].as_i64().unwrap() + amount;
|
||||
if new_amount > LIMIT_ITEMS {
|
||||
return true;
|
||||
}
|
||||
dataa["amount"] = new_amount.into();
|
||||
break;
|
||||
pub fn give_shop(master_item_id: i64, count: i64, user: &mut JsonValue) -> bool {
|
||||
give(
|
||||
&mut user["shop_list"],
|
||||
"master_shop_item_id",
|
||||
master_item_id,
|
||||
LIMIT_ITEMS,
|
||||
"count",
|
||||
count,
|
||||
object!{
|
||||
master_shop_item_id: master_item_id,
|
||||
count: count
|
||||
}
|
||||
}
|
||||
if !has {
|
||||
user["item_list"].push(object!{
|
||||
)
|
||||
}
|
||||
|
||||
pub fn give_item(master_item_id: i64, amount: i64, user: &mut JsonValue) -> bool {
|
||||
give(
|
||||
&mut user["item_list"],
|
||||
"master_item_id",
|
||||
master_item_id,
|
||||
LIMIT_ITEMS,
|
||||
"amount",
|
||||
amount,
|
||||
object!{
|
||||
id: master_item_id,
|
||||
master_item_id: master_item_id,
|
||||
amount: amount,
|
||||
expire_date_time: null
|
||||
}).unwrap();
|
||||
}
|
||||
false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn use_item(item: &JsonValue, multiplier: i64, user: &mut JsonValue) {
|
||||
@@ -175,25 +189,18 @@ pub fn give_points(master_item_id: i64, amount: i64, user: &mut JsonValue, missi
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut has = false;
|
||||
for data in user["point_list"].members_mut() {
|
||||
if data["type"].as_i64().unwrap() == master_item_id {
|
||||
has = true;
|
||||
let new_amount = data["amount"].as_i64().unwrap() + amount;
|
||||
if new_amount > LIMIT_COINS {
|
||||
return true;
|
||||
}
|
||||
data["amount"] = new_amount.into();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !has {
|
||||
user["point_list"].push(object!{
|
||||
give(
|
||||
&mut user["point_list"],
|
||||
"type",
|
||||
master_item_id,
|
||||
LIMIT_COINS,
|
||||
"amount",
|
||||
amount,
|
||||
object!{
|
||||
type: master_item_id,
|
||||
amount: amount
|
||||
}).unwrap();
|
||||
}
|
||||
false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn use_itemm(master_item_id: i64, amount: i64, user: &mut JsonValue) {
|
||||
@@ -210,10 +217,13 @@ pub fn use_itemm(master_item_id: i64, amount: i64, user: &mut JsonValue) {
|
||||
}
|
||||
|
||||
pub fn give_primogems(amount: i64, user: &mut JsonValue) -> bool {
|
||||
let new_amount = user["gem"]["free"].as_i64().unwrap() + amount;
|
||||
if new_amount > LIMIT_PRIMOGEMS {
|
||||
if user["gem"]["free"].as_i64().unwrap() >= LIMIT_PRIMOGEMS {
|
||||
return true;
|
||||
}
|
||||
let new_amount = user["gem"]["free"].as_i64().unwrap() + amount;
|
||||
if user["gem"]["free"].as_i64().unwrap() > LIMIT_PRIMOGEMS {
|
||||
user["gem"]["free"] = LIMIT_PRIMOGEMS.into();
|
||||
}
|
||||
|
||||
user["gem"]["free"] = new_amount.into();
|
||||
false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use json::{object, array, JsonValue};
|
||||
use actix_web::{HttpRequest};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::router::{global, userdata, items, databases};
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use json::{array, object, JsonValue};
|
||||
use actix_web::{HttpRequest};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
|
||||
use crate::router::{global, userdata, items, databases};
|
||||
use crate::encryption;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -440,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pub mod user;
|
||||
|
||||
use rusqlite::params;
|
||||
use lazy_static::lazy_static;
|
||||
use json::{JsonValue, array, object};
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use rand::RngExt;
|
||||
|
||||
use crate::router::global;
|
||||
use crate::router::items;
|
||||
@@ -17,16 +17,17 @@ lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
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 migration (
|
||||
token TEXT NOT NULL PRIMARY KEY,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS userdata (
|
||||
user_id BIGINT NOT NULL PRIMARY KEY,
|
||||
userdata TEXT NOT NULL,
|
||||
@@ -301,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::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() {
|
||||
@@ -479,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."));
|
||||
}
|
||||
@@ -490,7 +430,8 @@ pub fn webui_login(uid: i64, password: &str) -> Result<String, String> {
|
||||
let new_token = create_webui_token();
|
||||
|
||||
DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(uid));
|
||||
DATABASE.lock_and_exec("INSERT INTO webui (user_id, token, last_login) VALUES (?1, ?2, ?3)", params!(uid, new_token, global::timestamp()));
|
||||
// This could overflow given enough time... though rusqlite doesnt currently support ToSql on a u64...... maybe someday
|
||||
DATABASE.lock_and_exec("INSERT INTO webui (user_id, token, last_login) VALUES (?1, ?2, ?3)", params!(uid, new_token, global::timestamp() as i64));
|
||||
Ok(new_token)
|
||||
}
|
||||
|
||||
@@ -505,17 +446,16 @@ pub fn webui_import_user(user: JsonValue) -> Result<JsonValue, String> {
|
||||
let token = global::create_token();
|
||||
|
||||
DATABASE.lock_and_exec("INSERT INTO tokens (user_id, token) VALUES (?1, ?2)", params!(uid, token));
|
||||
let mig = crate::router::user::uid_to_code(uid.to_string());
|
||||
|
||||
save_acc_transfer(&mig, &user["password"].to_string());
|
||||
let token = user::migration::save_acc_transfer(uid, &user["password"].to_string());
|
||||
|
||||
Ok(object!{
|
||||
uid: uid,
|
||||
migration_token: mig
|
||||
migration_token: token
|
||||
})
|
||||
}
|
||||
|
||||
fn webui_login_token(token: &str) -> Option<String> {
|
||||
pub fn webui_login_token(token: &str) -> Option<String> {
|
||||
let uid = DATABASE.lock_and_select("SELECT user_id FROM webui WHERE token=?1", params!(token)).unwrap_or_default();
|
||||
if uid == String::new() || token.is_empty() {
|
||||
return None;
|
||||
@@ -640,7 +580,7 @@ pub fn purge_accounts() -> usize {
|
||||
DATABASE.lock_and_exec("DELETE FROM server_data WHERE user_id=?1", params!(user_id));
|
||||
DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(user_id));
|
||||
DATABASE.lock_and_exec("DELETE FROM tokens WHERE user_id=?1", params!(user_id));
|
||||
DATABASE.lock_and_exec("DELETE FROM migration WHERE token=?1", params!(crate::router::user::uid_to_code(user_id.to_string())));
|
||||
DATABASE.lock_and_exec("DELETE FROM migration WHERE user_id=?1", params!(user_id));
|
||||
}
|
||||
DATABASE.lock_and_exec("VACUUM", params!());
|
||||
crate::router::gree::vacuum_database();
|
||||
|
||||
1
src/router/userdata/user.rs
Normal file
1
src/router/userdata/user.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod migration;
|
||||
157
src/router/userdata/user/migration.rs
Normal file
157
src/router/userdata/user/migration.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use rusqlite::params;
|
||||
use json::{JsonValue, object};
|
||||
use crate::router::userdata;
|
||||
use rand::RngExt;
|
||||
use sha2::{Digest, Sha256};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
|
||||
fn generate_token() -> String {
|
||||
let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let mut rng = rand::rng();
|
||||
let random_string: String = (0..16)
|
||||
.map(|_| {
|
||||
let idx = rng.random_range(0..charset.len());
|
||||
charset.chars().nth(idx).unwrap()
|
||||
})
|
||||
.collect();
|
||||
random_string
|
||||
}
|
||||
|
||||
pub fn get_acc_transfer(token: &str, password: &str) -> JsonValue {
|
||||
let database = userdata::get_userdata_database();
|
||||
let data = database.lock_and_select("SELECT password FROM migration WHERE token=?1", params!(token));
|
||||
if data.is_err() {
|
||||
return object!{success: false};
|
||||
}
|
||||
if verify_password(password, &data.unwrap()) {
|
||||
let uid: i64 = database.lock_and_select_type("SELECT user_id FROM migration WHERE token=?1", params!(token)).unwrap();
|
||||
let login_token = userdata::get_login_token(uid);
|
||||
if login_token == String::new() {
|
||||
return object!{success: false};
|
||||
}
|
||||
return object!{success: true, login_token: login_token, user_id: uid};
|
||||
}
|
||||
object!{success: false}
|
||||
}
|
||||
|
||||
pub fn save_acc_transfer(uid: i64, password: &str) -> String {
|
||||
let database = userdata::get_userdata_database();
|
||||
let token = if let Ok(value) = database.lock_and_select("SELECT token FROM migration WHERE user_id=?1", params!(uid)) {
|
||||
value
|
||||
} else {
|
||||
generate_token()
|
||||
};
|
||||
database.lock_and_exec("DELETE FROM migration WHERE user_id=?1", params!(uid));
|
||||
database.lock_and_exec("INSERT INTO migration (user_id, token, password) VALUES (?1, ?2, ?3)", params!(uid, &token, hash_password(password)));
|
||||
token
|
||||
}
|
||||
|
||||
pub fn get_acc_token(uid: i64) -> String {
|
||||
let database = userdata::get_userdata_database();
|
||||
if let Ok(value) = database.lock_and_select("SELECT token FROM migration WHERE user_id=?1", params!(uid)) {
|
||||
value
|
||||
} else {
|
||||
save_acc_transfer(uid, "")
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_salt() -> Vec<u8> {
|
||||
let mut rng = rand::rng();
|
||||
let mut bytes = vec![0u8; 16];
|
||||
rng.fill(&mut bytes[..]);
|
||||
bytes
|
||||
}
|
||||
|
||||
fn hash_password(password: &str) -> String {
|
||||
if password.is_empty() { return String::new(); };
|
||||
let salt = &generate_salt();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password.as_bytes());
|
||||
hasher.update(salt);
|
||||
let hashed_password = hasher.finalize();
|
||||
|
||||
let salt_hash = [&salt[..], &hashed_password[..]].concat();
|
||||
general_purpose::STANDARD.encode(salt_hash)
|
||||
}
|
||||
|
||||
pub fn verify_password(password: &str, salted_hash: &str) -> bool {
|
||||
if password.is_empty() || salted_hash.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let bytes = general_purpose::STANDARD.decode(salted_hash);
|
||||
if bytes.is_err() {
|
||||
return password == salted_hash;
|
||||
}
|
||||
let bytes = bytes.unwrap();
|
||||
if bytes.len() < 17 {
|
||||
return password == salted_hash;
|
||||
}
|
||||
let (salt, hashed_password) = bytes.split_at(16);
|
||||
let hashed_password = &hashed_password[0..32];
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password.as_bytes());
|
||||
hasher.update(salt);
|
||||
let input_hash = hasher.finalize();
|
||||
|
||||
input_hash.as_slice() == hashed_password
|
||||
}
|
||||
|
||||
pub fn setup_sql(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
|
||||
conn.execute("
|
||||
CREATE TABLE IF NOT EXISTS migration (
|
||||
user_id BIGINT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, token)
|
||||
);
|
||||
", [])?;
|
||||
let is_updated = conn.prepare("SELECT user_id FROM migration LIMIT 1;").is_ok();
|
||||
if is_updated { return Ok(()); }
|
||||
println!("Upgrading migration table");
|
||||
conn.execute("DROP TABLE IF EXISTS migration_new;", [])?;
|
||||
conn.execute("
|
||||
CREATE TABLE migration_new (
|
||||
user_id BIGINT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, token)
|
||||
);
|
||||
", [])?;
|
||||
|
||||
let mut stmt = conn.prepare("SELECT token, password FROM migration")?;
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut insert_new_row = conn.prepare("INSERT INTO migration_new (user_id, token, password) VALUES (?1, ?2, ?3)")?;
|
||||
for row in rows {
|
||||
let (token, password) = row?;
|
||||
|
||||
let user_id = code_to_uid(&token);
|
||||
|
||||
insert_new_row.execute(params![user_id, token, password])?;
|
||||
}
|
||||
conn.execute("DROP TABLE migration;", params!())?;
|
||||
conn.execute("ALTER TABLE migration_new RENAME TO migration;", params!())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn code_to_uid(code: &str) -> String {
|
||||
code
|
||||
.replace('7', "")
|
||||
.replace('A', "1")
|
||||
.replace('G', "2")
|
||||
.replace('W', "3")
|
||||
.replace('Q', "4")
|
||||
.replace('Y', "5")
|
||||
.replace('6', "6")
|
||||
.replace('I', "7")
|
||||
.replace('P', "8")
|
||||
.replace('U', "9")
|
||||
.replace('M', "0")
|
||||
}
|
||||
@@ -5,6 +5,9 @@ use actix_web::{
|
||||
http::header::ContentType
|
||||
};
|
||||
use json::{JsonValue, object};
|
||||
use lazy_static::lazy_static;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use std::fs;
|
||||
|
||||
use crate::include_file;
|
||||
use crate::router::{userdata, items};
|
||||
@@ -32,10 +35,8 @@ fn error(msg: &str) -> HttpResponse {
|
||||
message: msg
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Access-Control-Allow-Origin", "*"))
|
||||
.insert_header(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
|
||||
}
|
||||
|
||||
pub fn login(_req: HttpRequest, body: String) -> HttpResponse {
|
||||
@@ -137,30 +138,61 @@ pub fn logout(req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Found()
|
||||
.insert_header(ContentType::json())
|
||||
.insert_header(("Set-Cookie", "ew_token=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT"))
|
||||
.insert_header(("Location", "/"))
|
||||
.insert_header(("Location", "/login.html"))
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
static WEBUI_ASSETS: Dir<'_> = include_dir!("webui/");
|
||||
|
||||
pub fn main(req: HttpRequest) -> HttpResponse {
|
||||
if req.path() == "/" {
|
||||
let path = if req.path().ends_with("/") { format!("{}index.html", req.path()) } else { req.path().to_string() };
|
||||
let mut chars = path.chars();
|
||||
chars.next();
|
||||
let path = chars.as_str();
|
||||
|
||||
if path == "login.html" {
|
||||
let token = get_login_token(&req);
|
||||
if token.is_some() {
|
||||
let data = userdata::webui_get_user(&token.unwrap());
|
||||
if data.is_some() {
|
||||
return HttpResponse::Found()
|
||||
.insert_header(("Location", "/home/"))
|
||||
.insert_header(("Location", "/account.html"))
|
||||
.body("");
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.path() != "/" && req.path() != "/home/" && req.path() != "/import/" && req.path() != "/help/" {
|
||||
return HttpResponse::Found()
|
||||
.insert_header(("Location", "/"))
|
||||
.body("");
|
||||
|
||||
if let Some(file) = WEBUI_ASSETS.get_file(&path) {
|
||||
let body = file.contents();
|
||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||
return HttpResponse::Ok()
|
||||
.insert_header(ContentType(mime))
|
||||
.insert_header(("content-length", body.len()))
|
||||
.body(body);
|
||||
} else if path.starts_with("webui/images/card-thumbnails") {
|
||||
let args = crate::get_args();
|
||||
|
||||
let file_name = path.split("/").last().unwrap_or("");
|
||||
let file_path = format!("{}/{}", args.image_asset_path, file_name).replace("//", "/");
|
||||
if args.image_asset_path != "" && let Ok(body) = fs::read(file_path) {
|
||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||
return HttpResponse::Ok()
|
||||
.insert_header(ContentType(mime))
|
||||
.insert_header(("content-length", body.len()))
|
||||
.body(body);
|
||||
} else {
|
||||
if args.image_asset_path != "" {
|
||||
println!("File '{file_name}' was requested, but no file was found on the disk!");
|
||||
}
|
||||
return HttpResponse::SeeOther()
|
||||
.insert_header(("location", format!("https://sif2-api.ethanthesleepy.one{}", req.path())))
|
||||
.body("");
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok()
|
||||
.insert_header(ContentType::html())
|
||||
.body(include_file!("webui/dist/index.html"))
|
||||
|
||||
HttpResponse::Found()
|
||||
.insert_header(("Location", "/"))
|
||||
.body("")
|
||||
}
|
||||
|
||||
pub fn export(req: HttpRequest) -> HttpResponse {
|
||||
@@ -199,7 +231,167 @@ pub fn server_info(_req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
.insert_header(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
|
||||
.insert_header(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
fn get_query_str(req: &HttpRequest, key: &str, def: &str) -> String {
|
||||
let query_str = req.query_string();
|
||||
query_str
|
||||
.split('&')
|
||||
.find(|s| s.starts_with(&format!("{key}=")))
|
||||
.and_then(|s| s.split('=').nth(1))
|
||||
.unwrap_or(def).to_string()
|
||||
}
|
||||
|
||||
pub fn get_card_info(req: HttpRequest) -> HttpResponse {
|
||||
let page = get_query_str(&req, "page", "1").parse::<usize>().unwrap_or(1) - 1;
|
||||
let max = get_query_str(&req, "max", "10").parse::<usize>().unwrap_or(10);
|
||||
let all = get_query_str(&req, "all", "false");
|
||||
let name_query = get_query_str(&req, "query", "");
|
||||
|
||||
let start = page * max;
|
||||
|
||||
let items = json::parse(&include_file!("src/router/webui/cards.json")).unwrap();
|
||||
|
||||
if all == "true" {
|
||||
let resp = object!{
|
||||
total_pages: 1,
|
||||
current: items
|
||||
};
|
||||
|
||||
return HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(json::stringify(resp));
|
||||
}
|
||||
|
||||
let mut filtered_items: Vec<_> = items.members().collect();
|
||||
|
||||
if !name_query.is_empty() {
|
||||
let lowercase_query = name_query.to_lowercase();
|
||||
filtered_items.retain(|item| {
|
||||
item["name"].to_string().to_lowercase().contains(&lowercase_query)
|
||||
});
|
||||
}
|
||||
|
||||
let total_len = filtered_items.len();
|
||||
|
||||
let page_items: Vec<_> = filtered_items
|
||||
.into_iter()
|
||||
.skip(start)
|
||||
.take(max)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if page_items.is_empty() {
|
||||
return HttpResponse::NotFound()
|
||||
.finish();
|
||||
}
|
||||
|
||||
let total_pages = (total_len as f64 / max as f64).ceil() as usize;
|
||||
let args = crate::get_args();
|
||||
|
||||
let resp = object!{
|
||||
total_pages: total_pages,
|
||||
current: page_items,
|
||||
image_path: args.image_asset_path
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
pub fn get_music_info(req: HttpRequest) -> HttpResponse {
|
||||
let page = get_query_str(&req, "page", "1").parse::<usize>().unwrap_or(1) - 1;
|
||||
let max = get_query_str(&req, "max", "10").parse::<usize>().unwrap_or(10);
|
||||
let lang = get_query_str(&req, "lang", "JP");
|
||||
|
||||
let start = page * max;
|
||||
|
||||
let items = if lang == "EN" {
|
||||
json::parse(&include_file!("src/router/databases/json/global/music.json")).unwrap()
|
||||
} else {
|
||||
json::parse(&include_file!("src/router/databases/json/music.json")).unwrap()
|
||||
};
|
||||
|
||||
let page_items: Vec<_> = items.members()
|
||||
.skip(start)
|
||||
.take(max)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if page_items.is_empty() {
|
||||
return HttpResponse::NotFound()
|
||||
.finish();
|
||||
}
|
||||
|
||||
let total_items = items.len();
|
||||
let total_pages = (total_items as f64 / max as f64).ceil() as usize;
|
||||
|
||||
let resp = object!{
|
||||
total_pages: total_pages,
|
||||
current: page_items
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ITEM: JsonValue = json::parse(&include_file!("src/router/webui/item.json")).unwrap();
|
||||
static ref LOGIN_BONUS: JsonValue = json::parse(&include_file!("src/router/webui/login_bonus.json")).unwrap();
|
||||
}
|
||||
|
||||
pub fn list_login_bonus(_req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(json::stringify(LOGIN_BONUS.clone()))
|
||||
}
|
||||
|
||||
pub fn list_items(_req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(json::stringify(ITEM.clone()))
|
||||
}
|
||||
|
||||
pub fn cheat(req: HttpRequest, _body: String) -> HttpResponse {
|
||||
let token = get_login_token(&req);
|
||||
if token.is_none() {
|
||||
return error("Not logged in");
|
||||
}
|
||||
let key = userdata::webui_login_token(&token.unwrap());
|
||||
if key.is_none() {
|
||||
return error("Not logged in");
|
||||
}
|
||||
let key = key.unwrap();
|
||||
let mut user = userdata::get_acc_home(&key);
|
||||
|
||||
for item in ITEM.entries() {
|
||||
let id = item.0.parse::<i32>().unwrap_or(0);
|
||||
let data = item.1;
|
||||
if id == 0 {
|
||||
continue;
|
||||
}
|
||||
let reward_type = data["reward_type"].as_i32().unwrap();
|
||||
let limit = if reward_type == 4 {
|
||||
items::LIMIT_COINS
|
||||
} else if reward_type == 1 {
|
||||
items::LIMIT_PRIMOGEMS
|
||||
} else {
|
||||
items::LIMIT_ITEMS
|
||||
};
|
||||
items::gift_item_basic(id, limit, reward_type, "You have cheated. Here are \"gifts\".", &mut user);
|
||||
}
|
||||
|
||||
userdata::save_acc_home(&key, user);
|
||||
|
||||
let resp = object!{
|
||||
result: "OK"
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
.insert_header(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
4626
src/router/webui/cards.json
Normal file
4626
src/router/webui/cards.json
Normal file
File diff suppressed because it is too large
Load Diff
1054
src/router/webui/item.json
Normal file
1054
src/router/webui/item.json
Normal file
File diff suppressed because it is too large
Load Diff
1262
src/router/webui/login_bonus.json
Normal file
1262
src/router/webui/login_bonus.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,13 @@ 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 = Connection::open(&self.path).unwrap();
|
||||
let mut stmt = conn.prepare(command)?;
|
||||
|
||||
@@ -5,20 +5,7 @@ use actix_web::{
|
||||
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()
|
||||
@@ -26,7 +13,29 @@ async fn maintenance(_req: HttpRequest) -> HttpResponse {
|
||||
.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);
|
||||
|
||||
|
||||
1
webui
Submodule
1
webui
Submodule
Submodule webui added at e5b5d3d84d
@@ -1,21 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
24
webui/.gitignore
vendored
24
webui/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EW private server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4554
webui/package-lock.json
generated
4554
webui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "webui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.2",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
const serverUrl = "";
|
||||
|
||||
async function api(url, body) {
|
||||
try {
|
||||
let options = body ? {method: "POST", body: JSON.stringify(body)} : {}
|
||||
const resp = await fetch(serverUrl + url, options);
|
||||
const text = await resp.text();
|
||||
return JSON.parse(text);
|
||||
} catch(e) {
|
||||
return {
|
||||
result: "ERR",
|
||||
message: e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default api;
|
||||
@@ -1,36 +0,0 @@
|
||||
body {
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
#home {
|
||||
width: 90%;
|
||||
margin: 50px auto;
|
||||
background-color: #43A047;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
#logout {
|
||||
border: none;
|
||||
text-align: center;
|
||||
text-decoration: underline;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
transition-duration: 0.4s;
|
||||
float: right;
|
||||
background-color: yellow;
|
||||
border-radius: 30px;
|
||||
padding: 5px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#logout:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
#error p {
|
||||
color: orange;
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useState, useParams, useEffect } from 'react'
|
||||
import './Help.css'
|
||||
import Request from '../Request.jsx'
|
||||
let init = false;
|
||||
|
||||
function Help() {
|
||||
const [downloadUrl, setDownloadUrl] = useState(<div>Your server admin has no pre-patched apks to download. See the question below..</div>);
|
||||
const [downloadUrliOSGL, setDownloadUrliOSGL] = useState("https://ethanthesleepy.one/public/lovelive/sif2/sif2-gl.ipa");
|
||||
const [downloadUrliOSJP, setDownloadUrliOSJP] = useState("https://ethanthesleepy.one/public/lovelive/sif2/sif2-jp.ipa");
|
||||
|
||||
const [assetUrl, setAssetUrl] = useState("https://sif2.sif.moe");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (init) return;
|
||||
init = true;
|
||||
let resp = await Request("/api/webui/serverInfo");
|
||||
if (resp.result !== "OK") {
|
||||
return;
|
||||
}
|
||||
if (!resp.data.links) return;
|
||||
if (resp.data.links.global && resp.data.links.japan) {
|
||||
setDownloadUrl(
|
||||
<div>Your server admin has a link to download! Download <a href={resp.data.links.japan}>Japan</a> or <a href={resp.data.links.global}>Global</a></div>
|
||||
);
|
||||
} else if (resp.data.links.global) {
|
||||
setDownloadUrl(
|
||||
<div>Your server admin has a link to download! Download <a href={resp.data.links.global}>Global</a></div>
|
||||
);
|
||||
} else if (resp.data.links.japan) {
|
||||
setDownloadUrl(
|
||||
<div>Your server admin has a link to download! Download <a href={resp.data.links.japan}>Japan</a></div>
|
||||
);
|
||||
}
|
||||
if (resp.data.links.assets) {
|
||||
setAssetUrl(resp.data.links.assets);
|
||||
}
|
||||
|
||||
if (!resp.data.links.ios) return;
|
||||
if (resp.data.links.ios.japan) {
|
||||
setDownloadUrliOSJP(resp.data.links.ios.japan);
|
||||
}
|
||||
if (resp.data.links.ios.global) {
|
||||
setDownloadUrliOSGL(resp.data.links.ios.global);
|
||||
}
|
||||
|
||||
})();
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="home">
|
||||
<h1>Help/About</h1>
|
||||
<h2>What is "ew"? What is this server for?</h2>
|
||||
<p>"ew" is a private server, written in Rust, for the short lived game "Love Live! School idol festival 2 MIRACLE LIVE!", a Love Live! themed mobile rhythm game.</p>
|
||||
|
||||
<h2>I'm just trying to play on this server, how do I install the app? (Android)</h2>
|
||||
<p>{downloadUrl}</p>
|
||||
|
||||
<h2>My server admin has no download URLs, how do I patch the apk? (Android)</h2>
|
||||
<p>You can use <a href="https://arasfon.ru/sif2/patcher/">@arasfon's sif2 apk patcher</a>. Plug and the asset url, which is "{assetUrl}", into the "Assets URL" textbox, and then the server url (Which is likely "{window.location.origin}", though this may not be the case). Select your game version, set "header format" to "Lowercase", and press patch. Once that done, use <a href="https://github.com/patrickfav/uber-apk-signer">uber-apk-signer</a> to sign the apk. Then, get it onto your phone and install!</p>
|
||||
|
||||
<h2>How do I install the app? (iOS)</h2>
|
||||
<p>Running on iOS is much simpler than Android, thanks to triangle on the discord. You first download an ipa file for <a href={downloadUrliOSGL}>global</a> or <a href={downloadUrliOSJP}>Japan</a>, and use <a href="https://sideloadly.io/">Sideloadly</a> (or your preferred application installer) to install the app. Then open settings, navigate to the app you just installed, and input the server url (Which is likely "{window.location.origin}", though this may not be the case), and the asset url, which is "{assetUrl}". If you have any errors opening the app, make sure none of the urls in settings end with a slash (/).</p>
|
||||
|
||||
<h2>Help! I'm trying to open the app and it shows as "unavailable" (iOS)</h2>
|
||||
<p>Do not delete it, Just re-sideload the app. This is an Apple security feature.</p>
|
||||
|
||||
<h2>How well does this work?</h2>
|
||||
<p>Works well enough. The server itself takes up not even 20mb of storage, and it's written in rust. I personally think it's pretty well written.</p>
|
||||
|
||||
<h2>Could my computer/laptop run a server?</h2>
|
||||
<p>Very very likely. If the platform is <a href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">supported by rust</a>, then the answer is yes! It is recommended to manually compile the project until I get the time to setup actions. <a href="https://git.ethanthesleepy.one/ethanaobrien/ew">ew repo</a></p>
|
||||
|
||||
<h2>Is the server down right now? I can't connect</h2>
|
||||
<p>Assuming you have just loaded this page on the server you use, then the answer is no, otherwise please contact your server admin.</p>
|
||||
|
||||
<h2>Do events work?</h2>
|
||||
<p>Most events do not, though most should not crash the game. Star events are partially implemented. You can get your rank up, and compete with other players in a ranking table, but no rewards are currently implemented.</p>
|
||||
|
||||
<h2>But then, how do I get event URs?</h2>
|
||||
<p>There are serial codes for several things, one of which includes all the event URs. I don't remember what does what but it is recommended to look at the serial code file to get the latest codes.</p>
|
||||
|
||||
<h2>Why does the game crash when I do x?</h2>
|
||||
<p>This likely means something on the server is broken. If you're self hosting, please contact me via matrix. Otherwise, contact your server admin and ask them to report the issue.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Help;
|
||||
@@ -1,36 +0,0 @@
|
||||
body {
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
#home {
|
||||
width: 90%;
|
||||
margin: 50px auto;
|
||||
background-color: #43A047;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
#logout {
|
||||
border: none;
|
||||
text-align: center;
|
||||
text-decoration: underline;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
transition-duration: 0.4s;
|
||||
float: right;
|
||||
background-color: yellow;
|
||||
border-radius: 30px;
|
||||
padding: 5px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#logout:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
#error p {
|
||||
color: orange;
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import { useState, useParams, useEffect } from 'react'
|
||||
import './Home.css'
|
||||
import Request from '../Request.jsx'
|
||||
let bonusItems = [];
|
||||
|
||||
function getMigrationID(uid) {
|
||||
return uid.toString()
|
||||
.replaceAll('1', "A")
|
||||
.replaceAll('2', "G")
|
||||
.replaceAll('3', "W")
|
||||
.replaceAll('4', "Q")
|
||||
.replaceAll('5', "Y")
|
||||
.replaceAll('6', "6")
|
||||
.replaceAll('7', "I")
|
||||
.replaceAll('8', "P")
|
||||
.replaceAll('9', "U")
|
||||
.replaceAll('0', "M")
|
||||
+ "7";
|
||||
}
|
||||
|
||||
function Bonus() {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const error = useState("");
|
||||
|
||||
let itemz = [];
|
||||
bonusItems.forEach((e) => {
|
||||
itemz.push(e.master_login_bonus_id);
|
||||
})
|
||||
|
||||
const [submittedItems, setSubmittedItems] = useState(itemz);
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
let input = parseInt(inputValue.trim());
|
||||
if (isNaN(input) || submittedItems.includes(input)) return;
|
||||
let resp = await Request("/api/webui/startLoginbonus", {
|
||||
bonus_id: input
|
||||
});
|
||||
if (resp.result !== "OK") {
|
||||
error[1](resp.message);
|
||||
return;
|
||||
}
|
||||
error[1]("");
|
||||
setSubmittedItems([...submittedItems, resp.id]);
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
const handleRemoveItem = (index) => {
|
||||
const updatedItems = [...submittedItems];
|
||||
updatedItems.splice(index, 1);
|
||||
setSubmittedItems(updatedItems);
|
||||
};
|
||||
// <button onClick={() => handleRemoveItem(index)}>X</button>
|
||||
return (
|
||||
<div>
|
||||
<h2>Current login bonus list</h2>
|
||||
<div id="error"> { error[0] ? <p>Error: { error[0] } </p> : <p></p> } </div>
|
||||
<ul>
|
||||
{submittedItems.map((item, index) => (
|
||||
<li key={index}>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
placeholder="Enter login bonus ID"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<p>You can find a list of available login bonus IDs <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/src/router/databases/json/login_bonus.json">here</a>. You should input the <code>id</code> field</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const [user, userdata] = useState();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [serverTime, setServerTime] = useState('');
|
||||
const error = useState("");
|
||||
|
||||
const logout = () => {
|
||||
window.location.href = "/webui/logout";
|
||||
}
|
||||
const downloadFile = (contents, name) => {
|
||||
let a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(new Blob([contents], {type: "application/json"}));
|
||||
a.download = name;
|
||||
a.click();
|
||||
}
|
||||
const expor = async (e) => {
|
||||
e.preventDefault();
|
||||
let resp = await Request("/api/webui/export");
|
||||
if (resp.result !== "OK") {
|
||||
error[1](resp.message);
|
||||
return;
|
||||
}
|
||||
downloadFile(resp.data.userdata, "userdata.json");
|
||||
downloadFile(resp.data.userhome, "userhome.json");
|
||||
downloadFile(resp.data.missions, "missions.json");
|
||||
downloadFile(resp.data.sifcards, "sifcards.json");
|
||||
|
||||
}
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
let time = Math.round(new Date(inputValue.trim()).getTime() / 1000);
|
||||
if (inputValue.trim() === "-1") {
|
||||
time = 1711741114;
|
||||
} else if (inputValue.trim() === "0") {
|
||||
time = 0;
|
||||
}
|
||||
if (time < 0 || isNaN(time)) return;
|
||||
let resp = await Request("/api/webui/set_time", {
|
||||
timestamp: time
|
||||
});
|
||||
if (resp.result !== "OK") {
|
||||
error[1](resp.message);
|
||||
return;
|
||||
}
|
||||
error[1]("");
|
||||
if (time === 0) {
|
||||
setServerTime("now");
|
||||
} else {
|
||||
setServerTime((new Date(time * 1000)).toString());
|
||||
}
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) return;
|
||||
(async () => {
|
||||
let resp = await Request("/api/webui/userInfo");
|
||||
if (resp.result !== "OK") {
|
||||
window.location.href = "/?message=" + encodeURIComponent(resp.message);
|
||||
return;
|
||||
}
|
||||
let user = resp.data.userdata;
|
||||
bonusItems = resp.data.loginbonus.bonus_list;
|
||||
/*
|
||||
bonusItems = [{"master_login_bonus_id":1,"day_counts":[1,2],"event_bonus_list":[]}];
|
||||
let user = {
|
||||
user: {
|
||||
id: 1,
|
||||
rank: 3,
|
||||
exp: 10,
|
||||
last_login_time: 5
|
||||
},
|
||||
time: new Date()
|
||||
}*/
|
||||
if (resp.data.time === 0) {
|
||||
setServerTime("now");
|
||||
} else {
|
||||
setServerTime((new Date(resp.data.time * 1000)).toString());
|
||||
}
|
||||
|
||||
userdata(
|
||||
<div>
|
||||
<p>User id: { user.user.id } </p>
|
||||
<p>Migration id: { getMigrationID(user.user.id) } </p>
|
||||
<p>Rank: { user.user.rank } ({ user.user.exp } exp)</p>
|
||||
<p>Last Login: { (new Date(user.user.last_login_time * 1000)).toString() } </p>
|
||||
<Bonus />
|
||||
</div>
|
||||
);
|
||||
})();
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="home">
|
||||
<button id="logout" onClick={expor}>Export account</button><br/><br/><br/>
|
||||
<button id="logout" onClick={logout}>Logout</button>
|
||||
<h1>Home</h1>
|
||||
|
||||
{ user ? <div>
|
||||
{ user }
|
||||
<h2>Server time</h2>
|
||||
<div id="error"> { error[0] ? <p>Error: { error[0] } </p> : <p></p> } </div>
|
||||
<p>Currently set to { serverTime }. Setting to 0 will set it to now, and -1 will reset it. Time will still progress, based off of when you set this timestamp.</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
placeholder="Enter Server Time"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form></div>
|
||||
: <p>Loading...</p> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -1,95 +0,0 @@
|
||||
body {
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
margin: 50px auto;
|
||||
background-color: green;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: black;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="file"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: dodgerblue;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: deepskyblue;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#sub_div p {
|
||||
color: red;
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
|
||||
#sub_div {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#sub_div button {
|
||||
width: 40%;
|
||||
padding: 12px;
|
||||
background-color: blue;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#sub_div button:hover {
|
||||
background-color: green;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import './Import.css'
|
||||
import Request from '../Request.jsx'
|
||||
|
||||
function Login() {
|
||||
const error = useState(new URL(window.location).searchParams.get("message") || "");
|
||||
const status = useState("");
|
||||
const uid = useState((window.localStorage && window.localStorage.getItem("ew_uid")) || "");
|
||||
let file=[], file1=[], file2=[], file3=[], password;
|
||||
let has_imported = false;
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!file[0] || !file1[0] || has_imported || !password) return;
|
||||
try {
|
||||
has_imported = true;
|
||||
let data = {
|
||||
userdata: JSON.parse(await file[0].text()),
|
||||
home: JSON.parse(await file1[0].text()),
|
||||
missions: file2[0] ? JSON.parse(await file2[0].text()) : undefined,
|
||||
sif_cards: file3[0] ? JSON.parse(await file3[0].text()) : undefined,
|
||||
password: password,
|
||||
jp: true
|
||||
};
|
||||
if (!data.userdata || !data.userdata.user || !data.userdata.user.id) {
|
||||
error[1]("Incorrect user data file format");
|
||||
return;
|
||||
}
|
||||
if (!data.home || !data.home.home || !data.home.home.information_list) {
|
||||
error[1]("Incorrect home data file format");
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data.missions) && data.missions) {
|
||||
error[1]("Incorrect mission data file format");
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data.sif_cards) && data.sif_cards) {
|
||||
error[1]("Incorrect sif card data file format");
|
||||
return;
|
||||
}
|
||||
let resp = await Request(
|
||||
"/api/webui/import",
|
||||
data
|
||||
);
|
||||
if (resp.result == "OK") {
|
||||
status[1](<div><p>Account imported!</p><p>User id: {resp.uid}</p><p>Migration token: {resp.migration_token}</p></div>);
|
||||
} else {
|
||||
error[1](resp.message);
|
||||
}
|
||||
} catch(e) {
|
||||
error[1](e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="login-form">
|
||||
<h1>Transfer</h1>
|
||||
<form>
|
||||
{ <p>{ status[0] } </p> }
|
||||
<label htmlFor="id">User data file (required):</label>
|
||||
<input type="file" id="id" name="id" onChange={(event) => {file = event.target.files}} accept="application/json"/>
|
||||
|
||||
<label htmlFor="file1">User Home data file (required):</label>
|
||||
<input type="file" id="file1" name="file1" onChange={(event) => {file1 = event.target.files}} accept="application/json"/>
|
||||
|
||||
<label htmlFor="file2">User Missions data file (optional):</label>
|
||||
<input type="file" id="file2" name="file2" onChange={(event) => {file2 = event.target.files}} accept="application/json"/>
|
||||
|
||||
<label htmlFor="file3">Sif cards data file (optional):</label>
|
||||
<input type="file" id="file3" name="file3" onChange={(event) => {file3 = event.target.files}} accept="application/json"/>
|
||||
|
||||
<label htmlFor="password">Transfer passcode (game will not recognize special characters, only use letters and numbers or you will be locked out):</label>
|
||||
<input type="password" id="password" name="password" onChange={(event) => {password = event.target.value}} />
|
||||
|
||||
<input type="submit" value="Submit" onClick={handleSubmit}/>
|
||||
<div id="sub_div">
|
||||
{ error[0] ? <p>Error: { error[0] }. Please reload the page and try again.</p> : <p></p> }
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
@@ -1,95 +0,0 @@
|
||||
body {
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
margin: 50px auto;
|
||||
background-color: green;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: black;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="text"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: dodgerblue;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: deepskyblue;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#sub_div p {
|
||||
color: red;
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
|
||||
#sub_div {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#sub_div button {
|
||||
width: 40%;
|
||||
padding: 12px;
|
||||
background-color: blue;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#sub_div button:hover {
|
||||
background-color: green;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import './Login.css'
|
||||
import Request from '../Request.jsx'
|
||||
|
||||
function Login() {
|
||||
const error = useState(new URL(window.location).searchParams.get("message") || "");
|
||||
const uid = useState((window.localStorage && window.localStorage.getItem("ew_uid")) || "");
|
||||
const password = useState("");
|
||||
|
||||
function showError(message) {
|
||||
error[1](message);
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!uid[0] || !password[0]) {
|
||||
showError("Missing userid/password");
|
||||
return;
|
||||
}
|
||||
if (isNaN(uid[0])) {
|
||||
showError("UserID should be a number. (The \"Friend ID\" in your profile)");
|
||||
return;
|
||||
}
|
||||
if (window.localStorage) window.localStorage.setItem("ew_uid", uid[0]);
|
||||
let resp = await Request(
|
||||
"/api/webui/login",
|
||||
{
|
||||
uid: parseInt(uid[0]),
|
||||
password: password[0]
|
||||
}
|
||||
);
|
||||
if (resp.result == "OK") {
|
||||
window.location.href = "/home/";
|
||||
} else {
|
||||
showError(resp.message);
|
||||
}
|
||||
};
|
||||
|
||||
const import_user = (e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = "/import/";
|
||||
}
|
||||
|
||||
const help = (e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = "/help/";
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="login-form">
|
||||
<h1>Login</h1>
|
||||
<form>
|
||||
<label htmlFor="id">SIF2 ID:</label>
|
||||
<input type="text" id="id" name="id" onChange={(event) => {uid[1](event.target.value)}} value={uid[0]} />
|
||||
<label htmlFor="password">Transfer passcode:</label>
|
||||
<input type="password" id="password" name="password" onChange={(event) => {password[1](event.target.value)}} />
|
||||
<input type="submit" value="Submit" onClick={handleSubmit}/>
|
||||
<div id="sub_div">
|
||||
<button onClick={import_user}>Import User</button><br/><br/>
|
||||
<button onClick={help}>Need help?</button><br/><br/>
|
||||
{ error[0] ? <p>Error: { error[0] } </p> : <p></p> }
|
||||
</div>
|
||||
<p>EW Version 1.0.0 - <a href="https://git.ethanthesleepy.one/ethanaobrien/ew">View source</a> - <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/LICENSE">View license</a></p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import Login from './login/Login.jsx'
|
||||
import Home from './home/Home.jsx'
|
||||
import Import from './import/Import.jsx'
|
||||
import Help from './help/Help.jsx'
|
||||
|
||||
let Elem;
|
||||
switch (window.location.pathname) {
|
||||
case "/":
|
||||
Elem = Login;
|
||||
break;
|
||||
case "/home/":
|
||||
Elem = Home;
|
||||
break;
|
||||
case "/import/":
|
||||
Elem = Import;
|
||||
break;
|
||||
case "/help/":
|
||||
Elem = Help;
|
||||
break;
|
||||
default:
|
||||
window.location.pathname = "/";
|
||||
}
|
||||
|
||||
if (Elem) {
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<Elem />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'index.js',
|
||||
assetFileNames: 'index.css',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user