Compare commits

..

8 Commits

Author SHA1 Message Date
Ethan O'Brien
6688655370 Add super cool Live Clear Rates webui 2025-12-24 21:10:37 -06:00
Ethan O'Brien
955e3819b5 Add music masterdata 2025-12-24 21:09:34 -06:00
Ethan O'Brien
591990a040 Track rage-quits as failed lives 2025-12-24 19:46:30 -06:00
Ethan O'Brien
0fc7f1d36e Redo migration table 2025-12-24 17:42:41 -06:00
Ethan O'Brien
15155bd8df Move migration related userdata functions to a new file 2025-12-24 14:34:13 -06:00
Ethan O'Brien
816589ae22 Fix deck size for new accounts 2025-12-24 14:18:31 -06:00
Ethan O'Brien
d14533d966 Tell workflow to checkout submodules 2025-12-24 13:21:49 -06:00
Ethan O'Brien
697f4188c2 Ship files for offline android/ios servers 2025-12-24 13:13:46 -06:00
19 changed files with 26125 additions and 137 deletions

View File

@@ -11,6 +11,8 @@ jobs:
steps:
- name: Checking out branch
uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust
shell: bash

3
.gitmodules vendored Normal file
View File

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

20
Cargo.lock generated
View File

@@ -642,6 +642,7 @@ dependencies = [
"hex",
"hmac",
"include-flate-codegen",
"include_dir",
"jni",
"json",
"lazy_static",
@@ -1021,6 +1022,25 @@ dependencies = [
"zstd",
]
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "indexmap"
version = "2.12.1"

View File

@@ -27,20 +27,21 @@ cbc = { version = "0.1.2", features = ["alloc"] }
aes = "0.8.4"
pem = "3.0.6"
ureq = "3.1.4"
include_dir = {version = "0.7.4", optional = true }
[target.aarch64-linux-android.dependencies]
jni = { version = "0.21.1", features = ["invocation", "default"] }
jni = { version = "0.21.1", 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"
# To enable this library you MUST comment out lib block below and add --features library
[features]
library = []
library = ["jni", "objc2", "objc2-foundation", "include_dir"]
#[lib]
#crate-type = ["cdylib"]

1
assets Submodule

Submodule assets added at dbb068e72c

View File

@@ -170,7 +170,7 @@ async fn api_req(req: HttpRequest, body: String) -> HttpResponse {
pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
let args = crate::get_args();
let headers = req.headers();
if args.hidden && req.path().starts_with("/api/webui/") {
if args.hidden && (req.path().starts_with("/api/webui/") || req.path().starts_with("/live_clear_rate.html")) {
return not_found(headers);
}
if headers.get("aoharu-asset-version").is_none() && req.path().starts_with("/api") && !req.path().starts_with("/api/webui") {
@@ -212,6 +212,7 @@ pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
"/v1.0/payment/balance" => gree::balance(req),
"/web/announcement" => web::announcement(req),
"/api/webui/userInfo" => webui::user(req),
"/live_clear_rate.html" => clear_rate::clearrate_html(req).await,
"/webui/logout" => webui::logout(req),
"/api/webui/export" => webui::export(req),
"/api/webui/serverInfo" => webui::server_info(req),

View File

@@ -1,5 +1,5 @@
use json::{object, array, JsonValue};
use actix_web::{HttpRequest};
use actix_web::{HttpRequest, HttpResponse, http::header::ContentType};
use rusqlite::params;
use std::sync::Mutex;
use lazy_static::lazy_static;
@@ -7,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)
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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(),
@@ -311,12 +308,12 @@ pub fn migration_code(req: HttpRequest) -> HttpResponse {
uid = uid_str.to_string();
}
}
let user = userdata::get_acc(&uid);
let resp = object!{
result: "OK",
migration_code: uid_to_code(user["user"]["id"].to_string())
migration_code: userdata::user::migration::get_acc_token(user["user"]["id"].as_i64().unwrap())
};
send(req, resp)
@@ -334,12 +331,12 @@ pub fn migration_password_register(req: HttpRequest, body: String) -> HttpRespon
uid = uid_str.to_string();
}
}
let user = userdata::get_acc(&uid);
let code = uid_to_code(user["user"]["id"].to_string());
let pass = decrypt_transfer_password(&body["migration_password"].to_string());
userdata::save_acc_transfer(&code, &pass);
userdata::user::migration::save_acc_transfer(user["user"]["id"].as_i64().unwrap(), &pass);
let resp = object!{
result: "OK"

View File

@@ -182,6 +182,10 @@ fn check_for_stale_data(server_data: &mut JsonValue, live_id: i64) {
if live["expire_date_time"].as_u64().unwrap() < curr_time || live["master_live_id"] == live_id {
expired.push(i).unwrap();
}
if live["expire_date_time"].as_u64().unwrap() < curr_time {
// User closed game after losing. Count this as a fail.
live_completed(live["master_live_id"].as_i64().unwrap(), live["level"].as_i32().unwrap(), true, 0, 0);
}
}
for i in expired.members() {
server_data["last_live_started"].array_remove(i.as_usize().unwrap());
@@ -225,8 +229,8 @@ fn start_live(login_token: &str, body: &JsonValue) {
}
check_for_stale_data(&mut server_data, body["master_live_id"].as_i64().unwrap());
let mut to_save = body.clone();
// The user has 24 hours to complete a live
to_save["expire_date_time"] = (global::timestamp() + (24 * 60 * 60)).into();
// The user has 1 hour to complete a live
to_save["expire_date_time"] = (global::timestamp() + (1 * 60 * 60)).into();
server_data["last_live_started"].push(to_save).unwrap();
userdata::save_server_data(login_token, server_data);

View File

@@ -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();
}
}

View File

@@ -1,9 +1,9 @@
pub mod user;
use rusqlite::params;
use lazy_static::lazy_static;
use json::{JsonValue, array, object};
use rand::Rng;
use sha2::{Digest, Sha256};
use base64::{Engine as _, engine::general_purpose};
use crate::router::global;
use crate::router::items;
@@ -17,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."));
}
@@ -505,13 +445,12 @@ pub fn webui_import_user(user: JsonValue) -> Result<JsonValue, String> {
let token = global::create_token();
DATABASE.lock_and_exec("INSERT INTO tokens (user_id, token) VALUES (?1, ?2)", params!(uid, token));
let mig = crate::router::user::uid_to_code(uid.to_string());
save_acc_transfer(&mig, &user["password"].to_string());
let token = user::migration::save_acc_transfer(uid, &user["password"].to_string());
Ok(object!{
uid: uid,
migration_token: mig
migration_token: token
})
}
@@ -640,7 +579,7 @@ pub fn purge_accounts() -> usize {
DATABASE.lock_and_exec("DELETE FROM server_data WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM tokens WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM migration WHERE token=?1", params!(crate::router::user::uid_to_code(user_id.to_string())));
DATABASE.lock_and_exec("DELETE FROM migration WHERE user_id=?1", params!(user_id));
}
DATABASE.lock_and_exec("VACUUM", params!());
crate::router::gree::vacuum_database();

View File

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

View File

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

View File

@@ -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)?;

View File

@@ -26,7 +26,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);