Compare commits

...

5 Commits

Author SHA1 Message Date
Ethan O'Brien
6523ad4d4a Remove old asset lists / masterdata endpoints 2026-07-26 23:28:05 -05:00
Ethan O'Brien
2056511bd1 Add missing gree endpoints 2026-07-26 23:22:23 -05:00
Ethan O'Brien
352ff2dbc5 Cleanly handle unverified gree devices 2026-07-26 23:15:02 -05:00
Ethan O'Brien
b5bd8ad4fb Sync new_skill.csv 2026-07-26 23:06:55 -05:00
Ethan O'Brien
3ca5dcf1ea Better support for custom card account flagging 2026-07-26 22:02:01 -05:00
19 changed files with 738 additions and 101201 deletions

View File

@@ -16,7 +16,7 @@ services:
#ENABLE_CUSTOM_SONGS: true # Custom songs are DISABLED by default; uncomment to enable upload/browse/download (webui + endpoints) #ENABLE_CUSTOM_SONGS: true # Custom songs are DISABLED by default; uncomment to enable upload/browse/download (webui + endpoints)
#PURGE: false # Purge dead user accounts on startup #PURGE: false # Purge dead user accounts on startup
#IMAGE_ASSET_PATH: /images/ # Images for cards in webui (will default to the public server) #IMAGE_ASSET_PATH: /images/ # Images for cards in webui (will default to the public server)
#MASTERDATA: /masterdata/ # Override bundled asset lists / CSVs / new_user.json at runtime (missing files fall back to the internal copies) #MASTERDATA: /masterdata/ # Override bundled CSVs / new_user.json at runtime (missing files fall back to the internal copies)
# Asset hash / version overrides. Leave unset to use the values bundled in # Asset hash / version overrides. Leave unset to use the values bundled in
# the binary; set one to make the server report a different hash/version # the binary; set one to make the server report a different hash/version

View File

@@ -60,9 +60,35 @@ fn vacuum_database() {
DATABASE.lock_and_exec("VACUUM", params!()); DATABASE.lock_and_exec("VACUUM", params!());
} }
pub fn get_user_cert(uuid: &str) -> Option<(i64, String)> {
let user_id = DATABASE.lock_and_select("SELECT user_id FROM users WHERE uuid=?1;", params!(uuid)).ok()?.parse::<i64>().ok()?;
let cert = DATABASE.lock_and_select("SELECT cert FROM users WHERE uuid=?1;", params!(uuid)).ok()?;
Some((user_id, cert))
}
pub fn is_registered(uuid: &str) -> bool {
match get_user_cert(uuid) {
Some((_, cert)) => pem::parse(&cert).is_ok(),
None => false
}
}
pub fn verify_fingerprint(uuid: &str, fingerprint: &str, cert: &str) -> bool {
let Ok(signature) = general_purpose::STANDARD.decode(fingerprint) else {
return false;
};
verify_signature(&signature, uuid.as_bytes(), cert)
}
fn verify_signature(signature: &[u8], message: &[u8], public_key: &str) -> bool { fn verify_signature(signature: &[u8], message: &[u8], public_key: &str) -> bool {
let pem = pem::parse(public_key).unwrap(); let Ok(pem) = pem::parse(public_key) else {
let public_key = RsaPublicKey::from_public_key_der(&pem.contents()).unwrap(); return false;
};
let Ok(public_key) = RsaPublicKey::from_public_key_der(&pem.contents()) else {
return false;
};
let digest = Sha1::digest(message); let digest = Sha1::digest(message);
public_key public_key
@@ -80,7 +106,9 @@ pub fn get_uuid(headers: &HeaderMap, body: &str) -> String {
return String::new(); return String::new();
} }
let cert = DATABASE.lock_and_select("SELECT cert FROM users WHERE user_id=?1;", params!(uid)).unwrap(); let Ok(cert) = DATABASE.lock_and_select("SELECT cert FROM users WHERE user_id=?1;", params!(uid)) else {
return String::new();
};
let data = format!("{}{}{}{}{}", uid, "sk1bdzb310n0s9tl", version, timestamp, body); let data = format!("{}{}{}{}{}", uid, "sk1bdzb310n0s9tl", version, timestamp, body);
let encoded = general_purpose::STANDARD.encode(data.as_bytes()); let encoded = general_purpose::STANDARD.encode(data.as_bytes());

View File

@@ -71,7 +71,7 @@ pub struct Args {
#[arg(long, default_value = "", help = "Path to image assets.")] #[arg(long, default_value = "", help = "Path to image assets.")]
pub image_asset_path: String, pub image_asset_path: String,
#[arg(long, default_value = "", help = "Optional directory to load asset lists and master data CSVs from at runtime. Layout mirrors the bundled assets (asset_lists/, csv/, csv-en/). Missing files fall back to the internal copies.")] #[arg(long, default_value = "", help = "Optional directory to load master data CSVs from at runtime. Layout mirrors the bundled assets (csv/, csv-en/). Missing files fall back to the internal copies.")]
pub masterdata: String pub masterdata: String
} }

View File

@@ -28,8 +28,6 @@ pub mod items;
pub mod databases; pub mod databases;
pub mod location; pub mod location;
pub mod event_ranking; pub mod event_ranking;
pub mod asset_lists;
mod master_data;
mod tools; mod tools;
use actix_web::{ use actix_web::{
@@ -50,6 +48,27 @@ pub struct Body(pub JsonValue);
pub struct Login(pub String); pub struct Login(pub String);
struct SessionError(HttpRequest);
impl std::fmt::Debug for SessionError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "invalid session for uid {}", global::get_uid(self.0.headers()))
}
}
impl std::fmt::Display for SessionError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl actix_web::ResponseError for SessionError {
fn error_response(&self) -> HttpResponse {
println!("Rejecting request from uid {}: bad session", global::get_uid(self.0.headers()));
global::api_error(&self.0, global::RESULT_SESSION)
}
}
pub struct Session { pub struct Session {
pub key: String, pub key: String,
pub body: JsonValue, pub body: JsonValue,
@@ -93,9 +112,14 @@ impl FromRequest for Login {
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let headers = req.headers().clone(); let headers = req.headers().clone();
let fut = String::from_request(req, payload); let req = req.clone();
let fut = String::from_request(&req, payload);
async move { async move {
Ok(Login(global::get_login(&headers, &encryption::decrypt_packet(&fut.await?).unwrap()))) let key = global::get_login(&headers, &encryption::decrypt_packet(&fut.await?).unwrap());
if key.is_empty() {
return Err(SessionError(req).into());
}
Ok(Login(key))
}.boxed_local() }.boxed_local()
} }
} }
@@ -106,10 +130,14 @@ impl FromRequest for Session {
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let headers = req.headers().clone(); let headers = req.headers().clone();
let fut = String::from_request(req, payload); let req = req.clone();
let fut = String::from_request(&req, payload);
async move { async move {
let body = encryption::decrypt_packet(&fut.await?).unwrap(); let body = encryption::decrypt_packet(&fut.await?).unwrap();
let key = global::get_login(&headers, &body); let key = global::get_login(&headers, &body);
if key.is_empty() {
return Err(SessionError(req).into());
}
Ok(Session { key, body: jzon::parse(&body).unwrap() }) Ok(Session { key, body: jzon::parse(&body).unwrap() })
}.boxed_local() }.boxed_local()
} }
@@ -212,8 +240,6 @@ pub fn configure(cfg: &mut actix_web::web::ServiceConfig) {
cfg.configure(crate::static_handlers::routes); cfg.configure(crate::static_handlers::routes);
cfg.service( cfg.service(
actix_web::web::scope("/api") actix_web::web::scope("/api")
.configure(asset_lists::routes)
.configure(master_data::routes)
.service( .service(
actix_web::web::scope("") actix_web::web::scope("")
.wrap(from_fn(webui_fallback)) .wrap(from_fn(webui_fallback))

View File

@@ -1,54 +0,0 @@
use actix_web::{HttpRequest, web, Responder, HttpResponse};
use actix_web::http::header::ContentType;
use jzon::object;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::sync::Mutex;
use crate::include_file;
lazy_static! {
static ref LIST_CACHE: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
}
pub fn routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/assetLists")
.route("/supported", web::get().to(supported))
.route("{platform}/{LANG}", web::get().to(get))
);
}
async fn get(_req: HttpRequest) -> impl Responder {
let mut response = object!{};
response["Bundle"] = load_list("Bundle").into();
response["Movie"] = load_list("Movie").into();
response["Sound"] = load_list("Sound").into();
let body = jzon::stringify(response);
HttpResponse::Ok()
.insert_header(("content-type", ContentType::json()))
.insert_header(("content-length", body.len()))
.body(body)
}
fn load_list(name: &str) -> String {
if let Some(cached) = LIST_CACHE.lock().unwrap().get(name) {
return cached.clone();
}
let rel = format!("asset_lists/{}.json", name);
let list = crate::runtime::read_masterdata_file(&rel)
.and_then(|b| String::from_utf8(b).ok())
.unwrap_or_else(|| match name {
"Bundle" => include_file!("src/router/asset_lists/Bundle.json"),
"Movie" => include_file!("src/router/asset_lists/Movie.json"),
"Sound" => include_file!("src/router/asset_lists/Sound.json"),
_ => unreachable!(),
});
LIST_CACHE.lock().unwrap().insert(name.to_string(), list.clone());
list
}
async fn supported() -> impl Responder {
"SUPPORTED"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,17 +13,23 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
} }
// Custom cards live at id prefix 10000+ (imported 10000-14999, new 15000+). // Custom cards live at id prefix 10000+ (imported 10000-14999, new 15000+).
// Like custom songs they need a protocol version (2, global::PROTOCOL_HEADER): // Like custom songs they need a protocol version (global::PROTOCOL_HEADER):
// clients below it can't resolve the ids // clients below it can't resolve the ids
pub const PROTOCOL_VERSION: u32 = 2;
pub fn is_custom(master_card_id: i64) -> bool { pub fn is_custom(master_card_id: i64) -> bool {
master_card_id >= 100_000_000 master_card_id >= 100_000_000
} }
pub fn client_supports_custom_cards(req: &HttpRequest) -> bool { pub fn client_supports_custom_cards(req: &HttpRequest) -> bool {
global::client_protocol_version(req) >= 2 global::client_protocol_version(req) >= PROTOCOL_VERSION
} }
pub fn owns_custom(user: &JsonValue) -> bool { pub fn account_supports_custom_cards(auth_key: &str) -> bool {
userdata::get_protocol_version(auth_key) >= PROTOCOL_VERSION
}
pub fn account_has_custom_cards(user: &JsonValue) -> bool {
user["card_list"].members().any(|card| is_custom(card["master_card_id"].as_i64().unwrap_or(0))) user["card_list"].members().any(|card| is_custom(card["master_card_id"].as_i64().unwrap_or(0)))
} }

View File

@@ -268,7 +268,14 @@ pub async fn ranking(req: HttpRequest, Session { key, body }: Session) -> impl R
let uid = data["user"].as_i64().unwrap(); let uid = data["user"].as_i64().unwrap();
let user = guest::get_user(uid, &object![], guest::UserView::Ranking, custom_cards); let user = guest::get_user(uid, &object![], guest::UserView::Ranking, custom_cards);
let user_obj = if uid == self_id { let user_obj = if uid == self_id {
userdata::get_acc_from_uid(uid)["user"].clone() // The client wants the fields get_user hides from other players
let mut self_user = object!{
user: userdata::get_acc_from_uid(uid)["user"].clone()
};
if !custom_cards {
guest::proxy_user_cards(&mut self_user);
}
self_user["user"].clone()
} else { } else {
user["user"].clone() user["user"].clone()
}; };

View File

@@ -40,27 +40,6 @@ fn region_subdir(region: Region) -> &'static str {
} }
} }
pub fn get_all(region: Region) -> JsonValue {
let mut rv = object!{};
for file in dir_for(region).files() {
let table_name = file.path()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default();
if table_name.is_empty() {
continue;
}
if let Some(bytes) = csv_bytes(region, table_name) {
rv[table_name] = String::from_utf8(bytes).unwrap_or_default().into();
}
}
rv
}
pub fn csv_bytes(region: Region, name: &str) -> Option<Vec<u8>> { pub fn csv_bytes(region: Region, name: &str) -> Option<Vec<u8>> {
let rel = format!("{}/{}.csv", region_subdir(region), name); let rel = format!("{}/{}.csv", region_subdir(region), name);
if let Some(bytes) = crate::runtime::read_masterdata_file(&rel) { if let Some(bytes) = crate::runtime::read_masterdata_file(&rel) {

File diff suppressed because it is too large Load Diff

View File

@@ -118,6 +118,8 @@ pub const RESULT_GAME_VERSION_UPDATED: i32 = 12;
pub const RESULT_RESOURCE_UPDATED: i32 = 13; pub const RESULT_RESOURCE_UPDATED: i32 = 13;
pub const RESULT_SESSION: i32 = 14;
pub const PROTOCOL_HEADER: &str = "X-Protocol-Version"; pub const PROTOCOL_HEADER: &str = "X-Protocol-Version";
pub fn client_protocol_version(req: &HttpRequest) -> u32 { pub fn client_protocol_version(req: &HttpRequest) -> u32 {
@@ -230,11 +232,7 @@ pub fn get_login(headers: &HeaderMap, body: &str) -> String {
Some(token) => { Some(token) => {
token token
}, },
None => { None => gree::get_uuid(headers, body),
let rv = gree::get_uuid(headers, body);
assert!(rv != String::new());
rv
},
} }
} }

View File

@@ -16,6 +16,8 @@ use crate::database::gree::*;
const APP_ID: &str = "232610769078541"; const APP_ID: &str = "232610769078541";
const SRC_APP_ID: &str = "100900301"; const SRC_APP_ID: &str = "100900301";
const HMAC_SECRET_HEX: &str = "6438663638653238346566646636306262616563326432323563306366643432"; const HMAC_SECRET_HEX: &str = "6438663638653238346566646636306262616563326432323563306366643432";
const ERROR_INVALID_SIGNATURE: i32 = 4;
const ERROR_MIGRATED_DEVICE: i32 = 20;
struct RequireGreeAuth; struct RequireGreeAuth;
@@ -51,6 +53,8 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
.service( .service(
web::scope("/migration") web::scope("/migration")
.route("", web::post().to(migration)) .route("", web::post().to(migration))
.route("/auth/initialize", web::post().to(initialize))
.route("/user/token", web::post().to(migration_user_token))
.route("/code", web::get().to(migration_code)) .route("/code", web::get().to(migration_code))
.route("/code/verify", web::post().to(migration_verify)) .route("/code/verify", web::post().to(migration_verify))
.route("/password/register", web::post().to(migration_password_register)) .route("/password/register", web::post().to(migration_password_register))
@@ -84,7 +88,7 @@ fn get_uid(req: &HttpRequest) -> String {
req.headers() req.headers()
.get("Authorization") .get("Authorization")
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.and_then(|auth| auth.split(",xoauth_requestor_id=\"").nth(1)) .and_then(|auth| auth.split("xoauth_requestor_id=\"").nth(1))
.and_then(|s| s.split('"').next()) .and_then(|s| s.split('"').next())
.unwrap_or("") .unwrap_or("")
.to_string() .to_string()
@@ -95,13 +99,13 @@ fn send(req: HttpRequest, resp: JsonValue) -> impl Responder {
jzon::stringify(resp) jzon::stringify(resp)
} }
async fn not_found() -> impl Responder { async fn not_found(req: HttpRequest) -> impl Responder {
let resp = object!{ let resp = object!{
code: 10001, code: 10001,
message: "Not Found", message: "Not Found",
result: "NG" result: "NG"
}; };
jzon::stringify(resp) send(req, resp)
} }
async fn initialize(req: HttpRequest, body: String) -> impl Responder { async fn initialize(req: HttpRequest, body: String) -> impl Responder {
@@ -119,8 +123,17 @@ async fn initialize(req: HttpRequest, body: String) -> impl Responder {
} }
async fn authorize(req: HttpRequest, _body: String) -> impl Responder { async fn authorize(req: HttpRequest, _body: String) -> impl Responder {
let resp = object!{ let resp = if is_registered(&get_uid(&req)) {
object!{
result: "OK" result: "OK"
}
} else {
println!("Unregistered device authorizing: {}", get_uid(&req));
object!{
result: "NG",
code: ERROR_MIGRATED_DEVICE,
message: "Device is not registered"
}
}; };
send(req, resp) send(req, resp)
@@ -224,6 +237,36 @@ async fn migration(req: HttpRequest, body: String) -> impl Responder {
send(req, resp) send(req, resp)
} }
async fn migration_user_token(req: HttpRequest, body: String) -> impl Responder {
let body = jzon::parse(&body).unwrap_or(object!{});
let uuid = body["uuid"].to_string();
let resp = if let Some((user_id, cert)) = get_user_cert(&uuid) {
if verify_fingerprint(&uuid, &body["fingerprint"].to_string(), &cert) {
update_cert(user_id, &body["token"].to_string());
object!{
result: "OK"
}
} else {
println!("Rejecting device token update for {}: bad fingerprint", uuid);
object!{
result: "NG",
code: ERROR_INVALID_SIGNATURE,
message: "Invalid fingerprint"
}
}
} else {
println!("Rejecting device token update for unregistered device {}", uuid);
object!{
result: "NG",
code: ERROR_MIGRATED_DEVICE,
message: "Device is not registered"
}
};
send(req, resp)
}
async fn balance(req: HttpRequest) -> impl Responder { async fn balance(req: HttpRequest) -> impl Responder {
let uid = get_uid(&req); let uid = get_uid(&req);

View File

@@ -1,31 +0,0 @@
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use actix_web::http::header::ContentType;
use crate::router::databases::csv::{get_all, Region};
pub fn routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/masterdata")
.route("/supported", web::get().to(supported))
.route("/{platform}/{LANG}", web::get().to(mst))
);
}
async fn mst(req: HttpRequest) -> impl Responder {
let lang = req.match_info().get("LANG").unwrap_or("JP");
let region = match lang.to_ascii_uppercase().as_str() {
"JP" => Region::Jp,
_ => Region::En, // idk
};
let body = get_all(region);
let body = jzon::stringify(body);
HttpResponse::Ok()
.insert_header(("content-type", ContentType::json()))
.insert_header(("content-length", body.len()))
.body(body)
}
async fn supported() -> impl Responder {
"SUPPORTED"
}

View File

@@ -25,7 +25,6 @@ fn get_asset_hash(req: &HttpRequest, body: &JsonValue) -> Option<String> {
} }
async fn asset_hash(req: HttpRequest, Body(body): Body) -> impl Responder { async fn asset_hash(req: HttpRequest, Body(body): Body) -> impl Responder {
match get_asset_hash(&req, &body) { match get_asset_hash(&req, &body) {
Some(hash) => global::api(&req, Some(object!{ Some(hash) => global::api(&req, Some(object!{
"asset_hash": hash "asset_hash": hash
@@ -35,7 +34,6 @@ async fn asset_hash(req: HttpRequest, Body(body): Body) -> impl Responder {
} }
async fn start(req: HttpRequest, Session { key, body }: Session) -> impl Responder { async fn start(req: HttpRequest, Session { key, body }: Session) -> impl Responder {
let Some(asset_hash) = get_asset_hash(&req, &body) else { let Some(asset_hash) = get_asset_hash(&req, &body) else {
return global::api_error(&req, global::RESULT_GAME_VERSION_UPDATED); return global::api_error(&req, global::RESULT_GAME_VERSION_UPDATED);
}; };
@@ -47,6 +45,11 @@ async fn start(req: HttpRequest, Session { key, body }: Session) -> impl Respond
user["user"]["last_login_time"] = global::timestamp().into(); user["user"]["last_login_time"] = global::timestamp().into();
userdata::save_acc(&key, user); userdata::save_acc(&key, user);
userdata::save_protocol_version(&key, global::client_protocol_version(&req));
if !crate::router::card::client_supports_custom_cards(&req) && crate::router::card::account_supports_custom_cards(&key) {
return global::api_error(&req, global::RESULT_GAME_VERSION_UPDATED); // todo - maybe compatibility layer?
}
global::api(&req, Some(object!{ global::api(&req, Some(object!{
"asset_hash": asset_hash, "asset_hash": asset_hash,

View File

@@ -65,15 +65,23 @@ pub enum UserView {
Ranking, Ranking,
} }
const DEFAULT_CARD: i64 = 10010001;
fn proxy_card_id(id: i64) -> i64 { fn proxy_card_id(id: i64) -> i64 {
let prefix = id / 10000; let prefix = id / 10000;
if prefix < 10000 { if prefix < 10000 {
id return id;
} else if prefix < 14000 { }
let rv = if prefix < 14000 {
(prefix - 9000) * 10000 + 1 (prefix - 9000) * 10000 + 1
} else { } else {
10010001 DEFAULT_CARD
};
// Not every prefix has a real character behind it
if crate::router::databases::CARD_LIST[rv.to_string()].is_empty() {
return DEFAULT_CARD;
} }
rv
} }
pub fn proxy_user_cards(user: &mut JsonValue) { pub fn proxy_user_cards(user: &mut JsonValue) {
@@ -83,25 +91,44 @@ pub fn proxy_user_cards(user: &mut JsonValue) {
user["user"][key] = proxy_card_id(id).into(); user["user"][key] = proxy_card_id(id).into();
} }
} }
// A card's id is its master_card_id, so both sides need proxying
for key in ["favorite_card", "guest_smile_card", "guest_cool_card", "guest_pure_card"] { for key in ["favorite_card", "guest_smile_card", "guest_cool_card", "guest_pure_card"] {
let id = user[key]["master_card_id"].as_i64().unwrap_or(0); let id = user[key]["master_card_id"].as_i64().unwrap_or(0);
if crate::router::card::is_custom(id) { if crate::router::card::is_custom(id) {
user[key]["id"] = proxy_card_id(id).into();
user[key]["master_card_id"] = proxy_card_id(id).into(); user[key]["master_card_id"] = proxy_card_id(id).into();
} }
} }
if !user["main_deck_detail"].is_empty() { if !user["main_deck_detail"].is_empty() {
let mut used = array![];
for id in user["main_deck_detail"]["deck"]["main_card_ids"].members_mut() { for id in user["main_deck_detail"]["deck"]["main_card_ids"].members_mut() {
let card = id.as_i64().unwrap_or(0); let card = proxy_card_id(id.as_i64().unwrap_or(0));
if crate::router::card::is_custom(card) { // Whole characters share one proxy, and the client can't hold the
*id = proxy_card_id(card).into(); // same card twice
if card == 0 || used.contains(card) {
*id = (0).into();
continue;
} }
used.push(card).unwrap();
*id = card.into();
} }
for card in user["main_deck_detail"]["card_list"].members_mut() { let mut cards = array![];
let mut ids = array![];
for card in user["main_deck_detail"]["card_list"].members() {
let id = card["master_card_id"].as_i64().unwrap_or(0); let id = card["master_card_id"].as_i64().unwrap_or(0);
if crate::router::card::is_custom(id) { let proxy = proxy_card_id(id);
card["master_card_id"] = proxy_card_id(id).into(); if ids.contains(proxy) {
continue;
} }
ids.push(proxy).unwrap();
let mut card = card.clone();
if proxy != id {
card["id"] = proxy.into();
card["master_card_id"] = proxy.into();
} }
cards.push(card).unwrap();
}
user["main_deck_detail"]["card_list"] = cards;
} }
} }

View File

@@ -59,11 +59,6 @@ async fn deck(Session { key, body }: Session) -> impl Responder {
async fn user(req: HttpRequest, Login(key): Login) -> impl Responder { async fn user(req: HttpRequest, Login(key): Login) -> impl Responder {
let mut user = userdata::get_acc(&key); let mut user = userdata::get_acc(&key);
// An account holding custom cards would break clients below protocol 2
if !crate::router::card::client_supports_custom_cards(&req) && crate::router::card::owns_custom(&user) {
return global::api_error(&req, global::RESULT_GAME_VERSION_UPDATED);
}
user["lottery_list"] = array![]; user["lottery_list"] = array![];
if crate::router::custom_song::client_supports_custom_songs(&req) { if crate::router::custom_song::client_supports_custom_songs(&req) {

View File

@@ -7,6 +7,7 @@ use rand::RngExt;
use crate::router::global; use crate::router::global;
use crate::router::items; use crate::router::items;
use crate::router::card;
use crate::database::custom_song; use crate::database::custom_song;
use crate::sql::SQLite; use crate::sql::SQLite;
use crate::include_file; use crate::include_file;
@@ -35,7 +36,8 @@ CREATE TABLE IF NOT EXISTS tokens (
CREATE TABLE IF NOT EXISTS userdata ( CREATE TABLE IF NOT EXISTS userdata (
user_id BIGINT NOT NULL PRIMARY KEY, user_id BIGINT NOT NULL PRIMARY KEY,
userdata TEXT NOT NULL, userdata TEXT NOT NULL,
friend_request_disabled INT NOT NULL friend_request_disabled INT NOT NULL,
protocol_version INT NOT NULL DEFAULT 0
); );
CREATE TABLE IF NOT EXISTS userhome ( CREATE TABLE IF NOT EXISTS userhome (
user_id BIGINT NOT NULL PRIMARY KEY, user_id BIGINT NOT NULL PRIMARY KEY,
@@ -84,8 +86,63 @@ CREATE TABLE IF NOT EXISTS webui (
); );
INSERT OR IGNORE INTO exchange (user_id, exchange) SELECT user_id, '[]' FROM userdata; INSERT OR IGNORE INTO exchange (user_id, exchange) SELECT user_id, '[]' FROM userdata;
").unwrap(); ").unwrap();
let is_updated = conn.prepare("SELECT protocol_version FROM userdata LIMIT 1;").is_ok();
if !is_updated {
println!("Upgrading userdata table");
conn.execute("ALTER TABLE userdata ADD COLUMN protocol_version INT NOT NULL DEFAULT 0;", []).unwrap();
}
} }
// maybe we will use this later
/*
pub fn downgrade_account_cards(user: &JsonValue) -> JsonValue {
let mut rv = user.clone();
let mut cards = array![];
let mut ids = array![];
for data in user["card_list"].members() {
let id = data["master_card_id"].as_i64().unwrap_or(0);
let downgraded = guest::proxy_card_id(id);
// Whole characters share one downgrade, and the client can't hold the
// same card twice
if ids.contains(downgraded) {
continue;
}
ids.push(downgraded).unwrap();
let mut data = data.clone();
if downgraded != id {
data["id"] = downgraded.into();
data["master_card_id"] = downgraded.into();
}
cards.push(data).unwrap();
}
rv["card_list"] = cards;
for deck in rv["deck_list"].members_mut() {
let mut used = array![];
for slot in deck["main_card_ids"].members_mut() {
let id = guest::proxy_card_id(slot.as_i64().unwrap_or(0));
// Cards the downgrade merged away leave the slot empty
if id == 0 || used.contains(id) {
*slot = (0).into();
continue;
}
used.push(id).unwrap();
*slot = id.into();
}
}
for key in ["favorite_master_card_id", "guest_smile_master_card_id", "guest_cool_master_card_id", "guest_pure_master_card_id"] {
let id = rv["user"][key].as_i64().unwrap_or(0);
rv["user"][key] = guest::proxy_card_id(id).into();
}
rv
}
*/
fn acc_exists(uid: i64) -> bool { fn acc_exists(uid: i64) -> bool {
DATABASE.lock_and_select("SELECT user_id FROM userdata WHERE user_id=?1", params!(uid)).is_ok() DATABASE.lock_and_select("SELECT user_id FROM userdata WHERE user_id=?1", params!(uid)).is_ok()
} }
@@ -120,10 +177,11 @@ fn add_user_to_database(uid: i64, user: JsonValue, user_home: JsonValue, user_mi
let missions = jzon::stringify(user_missions.clone()); let missions = jzon::stringify(user_missions.clone());
let cards = jzon::stringify(sif_cards.clone()); let cards = jzon::stringify(sif_cards.clone());
DATABASE.lock_and_exec("INSERT INTO userdata (user_id, userdata, friend_request_disabled) VALUES (?1, ?2, ?3)", params!( DATABASE.lock_and_exec("INSERT INTO userdata (user_id, userdata, friend_request_disabled, protocol_version) VALUES (?1, ?2, ?3, ?4)", params!(
uid, uid,
jzon::stringify(user.clone()), jzon::stringify(user.clone()),
user["user"]["friend_request_disabled"].as_i32().unwrap() user["user"]["friend_request_disabled"].as_i32().unwrap(),
if card::account_has_custom_cards(&user) { card::PROTOCOL_VERSION } else { 0 }
)); ));
DATABASE.lock_and_exec("INSERT INTO userhome (user_id, userhome) VALUES (?1, ?2)", params!( DATABASE.lock_and_exec("INSERT INTO userhome (user_id, userhome) VALUES (?1, ?2)", params!(
uid, uid,
@@ -325,6 +383,9 @@ pub fn get_acc_event(auth_key: &str) -> JsonValue {
pub fn get_acc_eventlogin(auth_key: &str) -> JsonValue { pub fn get_acc_eventlogin(auth_key: &str) -> JsonValue {
get_data(auth_key, "eventloginbonus") get_data(auth_key, "eventloginbonus")
} }
pub fn get_protocol_version(auth_key: &str) -> u32 {
DATABASE.lock_and_select("SELECT protocol_version FROM userdata WHERE user_id=?1", params!(get_key(auth_key))).unwrap_or_default().parse::<u32>().unwrap_or(0)
}
pub fn save_data(auth_key: &str, row: &str, data: JsonValue) { pub fn save_data(auth_key: &str, row: &str, data: JsonValue) {
let key = get_key(auth_key); let key = get_key(auth_key);
@@ -363,6 +424,10 @@ pub fn save_acc_chats(auth_key: &str, data: JsonValue) {
pub fn save_acc_exchange(auth_key: &str, data: JsonValue) { pub fn save_acc_exchange(auth_key: &str, data: JsonValue) {
save_data(auth_key, "exchange", data); save_data(auth_key, "exchange", data);
} }
pub fn save_protocol_version(auth_key: &str, version: u32) {
DATABASE.lock_and_exec("UPDATE userdata SET protocol_version=?1 WHERE user_id=?2 AND protocol_version<?1", params!(version as i64, get_key(auth_key)));
}
pub fn save_acc_sif(auth_key: &str, data: JsonValue) { pub fn save_acc_sif(auth_key: &str, data: JsonValue) {
save_data(auth_key, "sifcards", data); save_data(auth_key, "sifcards", data);
} }