use actix_web::{ HttpResponse, HttpRequest, http::header::HeaderValue, 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}; fn get_config() -> JsonValue { let args = crate::get_args(); object!{ import: !args.disable_imports, export: !args.disable_exports } } fn get_login_token(req: &HttpRequest) -> Option { let blank_header = HeaderValue::from_static(""); let cookies = req.headers().get("Cookie").unwrap_or(&blank_header).to_str().unwrap_or(""); if cookies.is_empty() { return None; } return Some(cookies.split("ew_token=").last().unwrap_or("").split(';').collect::>()[0].to_string()); } fn error(msg: &str) -> HttpResponse { let resp = object!{ result: "ERR", message: msg }; HttpResponse::Ok() .insert_header(ContentType::json()) .body(json::stringify(resp)) } pub fn login(_req: HttpRequest, body: String) -> HttpResponse { let body = json::parse(&body).unwrap(); let token = userdata::webui_login(body["uid"].as_i64().unwrap(), &body["password"].to_string()); if token.is_err() { return error(&token.unwrap_err()); } let resp = object!{ result: "OK" }; HttpResponse::Ok() .insert_header(ContentType::json()) .insert_header(("Set-Cookie", format!("ew_token={}; SameSite=Strict; HttpOnly", token.unwrap()))) .body(json::stringify(resp)) } pub fn import(_req: HttpRequest, body: String) -> HttpResponse { if !get_config()["import"].as_bool().unwrap() { return error("Importing accounts is disabled on this server."); } let body = json::parse(&body).unwrap(); let result = userdata::webui_import_user(body); if result.is_err() { return error(&result.unwrap_err()); } let result = result.unwrap(); let resp = object!{ result: "OK", uid: result["uid"].clone(), migration_token: result["migration_token"].clone() }; HttpResponse::Ok() .insert_header(ContentType::json()) .body(json::stringify(resp)) } pub fn user(req: HttpRequest) -> HttpResponse { let token = get_login_token(&req); if token.is_none() { return error("Not logged in"); } let data = userdata::webui_get_user(&token.unwrap()); if data.is_none() { return error("Expired login"); } let mut data = data.unwrap(); data["userdata"]["user"]["rank"] = items::get_user_rank_data(data["userdata"]["user"]["exp"].as_i64().unwrap())["rank"].clone(); let resp = object!{ result: "OK", data: data }; HttpResponse::Ok() .insert_header(ContentType::json()) .body(json::stringify(resp)) } pub fn start_loginbonus(req: HttpRequest, body: String) -> HttpResponse { let token = get_login_token(&req); if token.is_none() { return error("Not logged in"); } let body = json::parse(&body).unwrap(); let resp = userdata::webui_start_loginbonus(body["bonus_id"].as_i64().unwrap(), &token.unwrap()); HttpResponse::Ok() .insert_header(ContentType::json()) .body(json::stringify(resp)) } pub fn set_time(req: HttpRequest, body: String) -> HttpResponse { let token = get_login_token(&req); if token.is_none() { return error("Not logged in"); } let body = json::parse(&body).unwrap(); let resp = userdata::set_server_time(body["timestamp"].as_i64().unwrap(), &token.unwrap()); HttpResponse::Ok() .insert_header(ContentType::json()) .body(json::stringify(resp)) } pub fn logout(req: HttpRequest) -> HttpResponse { let token = get_login_token(&req); if token.is_some() { userdata::webui_logout(&token.unwrap()); } let resp = object!{ result: "OK" }; 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", "/login.html")) .body(json::stringify(resp)) } static WEBUI_ASSETS: Dir<'_> = include_dir!("webui/"); pub fn main(req: HttpRequest) -> HttpResponse { 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", "/account.html")) .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::Found() .insert_header(("Location", "/")) .body("") } pub fn export(req: HttpRequest) -> HttpResponse { if !get_config()["export"].as_bool().unwrap() { return error("Exporting accounts is disabled on this server."); } let token = get_login_token(&req); if token.is_none() { return error("Not logged in"); } let resp = object!{ result: "OK", data: userdata::export_user(&token.unwrap()).unwrap() }; HttpResponse::Ok() .insert_header(ContentType::json()) .body(json::stringify(resp)) } pub fn server_info(_req: HttpRequest) -> HttpResponse { let args = crate::get_args(); let resp = object!{ result: "OK", data: { account_import: get_config()["import"].as_bool().unwrap(), links: { global: args.global_android, japan: args.japan_android, ios: { global: args.global_ios, japan: args.japan_ios }, assets: args.assets_url } } }; HttpResponse::Ok() .insert_header(ContentType::json()) .body(json::stringify(resp)) } fn get_query_str(req: &HttpRequest, key: &str, def: &str) -> String { let query_str = req.query_string(); query_str .split('&') .find(|s| s.starts_with(&format!("{key}="))) .and_then(|s| s.split('=').nth(1)) .unwrap_or(def).to_string() } pub fn get_card_info(req: HttpRequest) -> HttpResponse { let page = get_query_str(&req, "page", "1").parse::().unwrap_or(1) - 1; let max = get_query_str(&req, "max", "10").parse::().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::().unwrap_or(1) - 1; let max = get_query_str(&req, "max", "10").parse::().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::().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)) }