feat: stuff
This commit is contained in:
parent
6d58fd47a1
commit
a40b1498e2
16 changed files with 2992 additions and 256 deletions
|
|
@ -1,199 +0,0 @@
|
|||
use chrono::{Datelike, NaiveDate};
|
||||
use rocket::{post, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::account::account::{generate_password, User};
|
||||
use crate::error::Errors;
|
||||
use crate::nnid::pid_distribution::next_pid;
|
||||
use crate::Pool;
|
||||
use crate::xml::{Xml, YesNoVal};
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Email{
|
||||
address: Box<str>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Mii{
|
||||
name: Box<str>,
|
||||
primary: YesNoVal,
|
||||
data: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
pub struct AccountCreationData{
|
||||
birth_date: NaiveDate,
|
||||
user_id: Box<str>,
|
||||
password: Box<str>,
|
||||
country: Box<str>,
|
||||
language: Box<str>,
|
||||
tz_name: Box<str>,
|
||||
email: Email,
|
||||
mii: Mii,
|
||||
gender: Box<str>,
|
||||
marketing_flag: YesNoVal,
|
||||
off_device_flag: YesNoVal,
|
||||
region: i32
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
pub struct AccountCreationResponseData{
|
||||
pid: i32
|
||||
}
|
||||
|
||||
#[post("/v1/api/people", data="<data>")]
|
||||
pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Option<Errors>>{
|
||||
let database = database.inner();
|
||||
|
||||
// its fine to crash here if we cant get the next pid as that is in my opinion a dead state
|
||||
// anyways as noone can register anymore, EVER
|
||||
|
||||
let pid = next_pid(database).await;
|
||||
|
||||
let AccountCreationData {
|
||||
user_id,
|
||||
password,
|
||||
birth_date,
|
||||
tz_name,
|
||||
language,
|
||||
email: Email{
|
||||
address
|
||||
},
|
||||
mii: Mii{
|
||||
data,
|
||||
..
|
||||
},
|
||||
marketing_flag,
|
||||
gender,
|
||||
region,
|
||||
country,
|
||||
off_device_flag,
|
||||
..
|
||||
} = data.0;
|
||||
|
||||
|
||||
let password = generate_password(pid, &password).ok_or(None)?;
|
||||
|
||||
sqlx::query!("
|
||||
INSERT INTO users (
|
||||
pid,
|
||||
username,
|
||||
password,
|
||||
birthdate,
|
||||
timezone,
|
||||
email,
|
||||
country,
|
||||
language,
|
||||
marketing_allowed,
|
||||
off_device_allowed,
|
||||
region,
|
||||
gender,
|
||||
mii_data
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13
|
||||
)
|
||||
",
|
||||
pid,
|
||||
user_id.as_ref(),
|
||||
password,
|
||||
birth_date,
|
||||
tz_name.as_ref(),
|
||||
address.as_ref(),
|
||||
country.as_ref(),
|
||||
language.as_ref(),
|
||||
marketing_flag.0,
|
||||
off_device_flag.0,
|
||||
region,
|
||||
gender.as_ref(),
|
||||
data.as_ref()
|
||||
).execute(database).await.unwrap();
|
||||
|
||||
|
||||
|
||||
Ok(
|
||||
Xml(AccountCreationResponseData{
|
||||
pid
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use chrono::NaiveDate;
|
||||
use crate::nnid::create_account::AccountCreationData;
|
||||
|
||||
const TEST_XML: &str =
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>
|
||||
<person>
|
||||
<birth_date>1991-02-03</birth_date>
|
||||
<user_id>testtest</user_id>
|
||||
<password>[PASSWORD]</password>
|
||||
<country>DE</country>
|
||||
<language>en</language>
|
||||
<tz_name>Europe/Berlin</tz_name>
|
||||
<agreement>
|
||||
<agreement_date>2025-02-24T19:42:45</agreement_date>
|
||||
<country>US</country>
|
||||
<location>https://account.spfn.cc/v1/api/content/agreements/Nintendo-Network-EULA/0300</location>
|
||||
<type>NINTENDO-NETWORK-EULA</type>
|
||||
<version>0300</version>
|
||||
</agreement>
|
||||
<email>
|
||||
<address>tvnebel@gmail.com</address>
|
||||
<owned>N</owned>
|
||||
<parent>N</parent>
|
||||
<primary>Y</primary>
|
||||
<validated>N</validated>
|
||||
<type>DEFAULT</type>
|
||||
</email>
|
||||
<mii>
|
||||
<name>y</name>
|
||||
<primary>Y</primary>
|
||||
<data>
|
||||
AwAAQDrPvmeBxJIQ3j+V8Ip4iCWDvgAAAEB5AAAAIABOAEEATQBFAAAAAAAAAEBAAAAhAQJoRBgm
|
||||
NEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAANzO
|
||||
</data>
|
||||
</mii>
|
||||
<parental_consent>
|
||||
<scope>1</scope>
|
||||
<consent_date>2025-02-24T19:42:45</consent_date>
|
||||
<approval_id>0</approval_id>
|
||||
</parental_consent>
|
||||
<gender>M</gender>
|
||||
<region>1309343744</region>
|
||||
<marketing_flag>N</marketing_flag>
|
||||
<device_attributes>
|
||||
<device_attribute>
|
||||
<name>uuid_account</name>
|
||||
<value>55fdbad0-f2ab-11ef-b648-010144cdca06</value>
|
||||
</device_attribute>
|
||||
<device_attribute>
|
||||
<name>uuid_common</name>
|
||||
<value>898ed052-5e25-11ef-b648-010144cdca06</value>
|
||||
</device_attribute>
|
||||
<device_attribute>
|
||||
<name>persistent_id</name>
|
||||
<value>8000001d</value>
|
||||
</device_attribute>
|
||||
<device_attribute>
|
||||
<name>transferable_id_base</name>
|
||||
<value>0800000444cdca06</value>
|
||||
</device_attribute>
|
||||
<device_attribute>
|
||||
<name>transferable_id_base_common</name>
|
||||
<value>0640000444cdca06</value>
|
||||
</device_attribute>
|
||||
</device_attributes>
|
||||
<off_device_flag>N</off_device_flag>
|
||||
</person>";
|
||||
#[test]
|
||||
fn test(){
|
||||
let data: AccountCreationData = quick_xml::de::from_str(TEST_XML).unwrap();
|
||||
|
||||
assert_eq!(data.birth_date, NaiveDate::from_ymd_opt(1991,02,03).unwrap());
|
||||
assert_eq!(data.user_id.as_ref(), "testtest");
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@ pub mod agreements;
|
|||
pub mod timezones;
|
||||
pub mod person_exists;
|
||||
pub mod email;
|
||||
pub mod create_account;
|
||||
pub mod oauth;
|
||||
mod pid_distribution;
|
||||
pub mod people;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use rocket::{post, FromForm, State};
|
||||
use rocket::form::Form;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::account::account::User;
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::nnid::oauth::TokenData;
|
||||
use crate::Pool;
|
||||
use crate::xml::Xml;
|
||||
|
||||
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors = Errors{
|
||||
error: &[
|
||||
|
|
@ -31,8 +33,75 @@ pub struct TokenRequestData<'a>{
|
|||
password_type: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TokenReturnData {
|
||||
token: String,
|
||||
refresh_token: String,
|
||||
expires_in: i32
|
||||
}
|
||||
|
||||
impl TokenReturnData {
|
||||
async fn create_token(pid: i32, pool: &Pool, is_refresh_token: bool) -> (i64, i32){
|
||||
let token_type = if is_refresh_token{
|
||||
0x0
|
||||
} else {
|
||||
0x1
|
||||
};
|
||||
let data = sqlx::query!(
|
||||
"insert into tokens (token_type, pid)
|
||||
values ($1, $2) returning token_id, random",
|
||||
token_type, pid
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await.unwrap();
|
||||
|
||||
(data.token_id, data.random)
|
||||
}
|
||||
async fn create_regular_token(pid: i32, pool: &Pool) -> (i64, i32){
|
||||
Self::create_token(pid, pool, false).await
|
||||
}
|
||||
|
||||
async fn create_refresh_token(pid: i32, pool: &Pool) -> (i64, i32){
|
||||
Self::create_token(pid, pool, true).await
|
||||
}
|
||||
|
||||
async fn new(pid: i32, pool: &Pool) -> Self{
|
||||
let (token_id, random) = Self::create_regular_token(pid, pool).await;
|
||||
|
||||
let token = TokenData {
|
||||
token_id,
|
||||
random,
|
||||
pid
|
||||
};
|
||||
|
||||
let token = token.encode().to_string();
|
||||
|
||||
let (token_id, random) = Self::create_refresh_token(pid, pool).await;
|
||||
|
||||
let refresh_token = TokenData {
|
||||
token_id,
|
||||
random,
|
||||
pid
|
||||
};
|
||||
|
||||
let refresh_token = refresh_token.encode().to_string();
|
||||
|
||||
Self{
|
||||
token,
|
||||
refresh_token,
|
||||
expires_in: 3600
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename="OAuth20")]
|
||||
pub struct TokenRequestReturnData{
|
||||
access_token: TokenReturnData
|
||||
}
|
||||
|
||||
#[post("/v1/api/oauth20/access_token/generate", data="<data>")]
|
||||
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>) -> Result<(), Option<Errors<'static>>>{
|
||||
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>>{
|
||||
let pool = pool.inner();
|
||||
|
||||
let user = User::get_by_username(data.user_id, pool).await
|
||||
|
|
@ -46,7 +115,11 @@ pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>
|
|||
return Err(Some(ACCOUNT_BANNED_ERRORS));
|
||||
}
|
||||
|
||||
|
||||
let access_token = TokenReturnData::new(user.pid, pool).await;
|
||||
|
||||
Ok(())
|
||||
|
||||
|
||||
Ok(Xml(TokenRequestReturnData{
|
||||
access_token
|
||||
}))
|
||||
}
|
||||
|
|
@ -1 +1,103 @@
|
|||
pub mod generate_token;
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use aes::{Aes128, Aes256, Block};
|
||||
use aes::cipher::consts::{U32, U64};
|
||||
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, Iv, Key};
|
||||
use aes::cipher::generic_array::sequence::GenericSequence;
|
||||
use bytemuck::{bytes_of, bytes_of_mut, from_bytes, from_bytes_mut, Pod, Zeroable};
|
||||
use chrono::NaiveTime;
|
||||
use hmac::{Hmac, Mac};
|
||||
use md5::Md5;
|
||||
use once_cell::sync::Lazy;
|
||||
use aes::cipher::KeyIvInit;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
|
||||
pub mod generate_token;
|
||||
|
||||
#[derive(Pod, Zeroable, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct TokenData{
|
||||
pub pid: i32,
|
||||
pub random: i32,
|
||||
pub token_id: i64
|
||||
}
|
||||
|
||||
|
||||
static HMAC_SECRET: Lazy<Key<HmacMd5>> = Lazy::new(||{
|
||||
Key::<HmacMd5>::clone_from_slice(&hex::decode(
|
||||
env::var("ACCOUNT_HMAC_SECRET").expect("hmac secret has not been set")
|
||||
).expect("unable to decode ACCOUNT_HMAC_SECRET"))
|
||||
});
|
||||
|
||||
static AES_KEY: Lazy<Key<Aes128>> = Lazy::new(||{
|
||||
Key::<Aes128>::clone_from_slice(&hex::decode(
|
||||
env::var("ACCOUNT_AES_KEY").expect("hmac secret has not been set")
|
||||
).expect("unable to decode ACCOUNT_AES_KEY"))
|
||||
});
|
||||
|
||||
type HmacMd5 = Hmac<Md5>;
|
||||
type Aes128CbcEnc = cbc::Encryptor<Aes128>;
|
||||
type Aes128CbcDec = cbc::Decryptor<Aes128>;
|
||||
|
||||
impl TokenData{
|
||||
pub fn decode(token: &str) -> Option<Self>{
|
||||
let mut data = BASE64_STANDARD.decode(token).ok()?;
|
||||
|
||||
let data: [u8; 16] = data.try_into().ok()?;
|
||||
|
||||
let empty_iv = Iv::<Aes128CbcEnc>::generate(|_| 0);
|
||||
|
||||
let mut aes= Aes128CbcDec::new(&*AES_KEY, &empty_iv);
|
||||
|
||||
let mut block = Block::from(data);
|
||||
|
||||
aes.decrypt_block_mut(&mut block);
|
||||
|
||||
let data = block.as_slice();
|
||||
|
||||
let token_data: &TokenData = from_bytes(data);
|
||||
|
||||
Some(*token_data)
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Box<str>{
|
||||
let data = bytes_of(self);
|
||||
let data: [u8; 16] = data.try_into().unwrap();
|
||||
|
||||
let mut block = Block::from(data);
|
||||
|
||||
let empty_iv = Iv::<Aes128CbcEnc>::generate(|_| 0);
|
||||
|
||||
let mut aes= Aes128CbcEnc::new(&*AES_KEY, &empty_iv);
|
||||
|
||||
aes.encrypt_block_mut(&mut block);
|
||||
|
||||
let data = block.as_slice();
|
||||
|
||||
BASE64_STANDARD.encode(data).into_boxed_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use std::env;
|
||||
use crate::nnid::oauth::{TokenData, AES_KEY};
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode(){
|
||||
unsafe{ env::set_var("ACCOUNT_AES_KEY", "0123456789abcdef0123456789abcdef"); }
|
||||
|
||||
let token_data = TokenData{
|
||||
pid: 1,
|
||||
random: 2,
|
||||
token_id: 3
|
||||
};
|
||||
|
||||
let enc_data = token_data.encode();
|
||||
|
||||
let decrypted_token = TokenData::decode(&enc_data).unwrap();
|
||||
|
||||
assert_eq!(token_data, decrypted_token)
|
||||
}
|
||||
}
|
||||
347
src/nnid/people.rs
Normal file
347
src/nnid/people.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
use std::env;
|
||||
use std::io::Cursor;
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use gxhash::{gxhash32, gxhash64};
|
||||
use minio::s3::args::PutObjectArgs;
|
||||
use minio::s3::builders::{ObjectContent, SegmentedBytes};
|
||||
use minio::s3::client::ClientBuilder;
|
||||
use minio::s3::creds::StaticProvider;
|
||||
use minio::s3::http::BaseUrl;
|
||||
use minio::s3::utils::crc32;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::{get, post, put, State};
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use crate::account::account::{generate_password, Auth, User};
|
||||
use crate::dsresponse::Ds;
|
||||
use crate::error::Errors;
|
||||
use crate::nnid::pid_distribution::next_pid;
|
||||
use crate::nnid::timezones::{OFFSET_FROM_TIMEZONE, ZONE_TO_TIMEZONES};
|
||||
use crate::Pool;
|
||||
use crate::xml::{Xml, YesNoVal};
|
||||
|
||||
static S3_URL_STRING: Lazy<Box<str>> = Lazy::new(||
|
||||
env::var("S3_URL").expect("S3_URL not specified").into_boxed_str()
|
||||
);
|
||||
|
||||
|
||||
static S3_URL: Lazy<BaseUrl> = Lazy::new(||
|
||||
S3_URL_STRING.parse().unwrap()
|
||||
);
|
||||
|
||||
static S3_USER: Lazy<Box<str>> = Lazy::new(||
|
||||
env::var("S3_USER").expect("S3_USER not specified").into_boxed_str()
|
||||
);
|
||||
|
||||
static S3_PASSWD: Lazy<Box<str>> = Lazy::new(||
|
||||
env::var("S3_PASSWD").expect("S3_PASSWD not specified").into_boxed_str()
|
||||
);
|
||||
|
||||
fn get_mii_img_url_path(pid: i32, format: &str) -> String{
|
||||
format!("mii/{}/main.{}", pid, format)
|
||||
}
|
||||
|
||||
fn get_mii_img_url(pid: i32, format: &str) -> String{
|
||||
format!("{}/pn-boss/{}", &*S3_URL_STRING, get_mii_img_url_path(pid, format))
|
||||
}
|
||||
|
||||
async fn generate_s3_images(pid: i32, mii_data: &str){
|
||||
|
||||
|
||||
let auth = StaticProvider::new(&S3_USER, &S3_PASSWD, None);
|
||||
|
||||
let Ok(client) = ClientBuilder::new(S3_URL.clone())
|
||||
.provider(Some(Box::new(auth)))
|
||||
.build() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(image) = mii::get_image_png(mii_data).await else {
|
||||
return;
|
||||
};
|
||||
let object_name = get_mii_img_url_path(pid, "png");
|
||||
let object_content = ObjectContent::from(image);
|
||||
client.put_object_content("pn-cdn", &object_name, object_content).send().await.ok();
|
||||
|
||||
let Some(image) = mii::get_image_tga(mii_data).await else {
|
||||
return;
|
||||
};
|
||||
let object_name = get_mii_img_url_path(pid, "tga");
|
||||
let object_content = ObjectContent::from(image);
|
||||
client.put_object_content("pn-cdn", &object_name, object_content).send().await.ok();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Email{
|
||||
address: Box<str>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Mii{
|
||||
name: Box<str>,
|
||||
primary: YesNoVal,
|
||||
data: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
pub struct AccountCreationData{
|
||||
birth_date: NaiveDate,
|
||||
user_id: Box<str>,
|
||||
password: Box<str>,
|
||||
country: Box<str>,
|
||||
language: Box<str>,
|
||||
tz_name: Box<str>,
|
||||
email: Email,
|
||||
mii: Mii,
|
||||
gender: Box<str>,
|
||||
marketing_flag: YesNoVal,
|
||||
off_device_flag: YesNoVal,
|
||||
region: i32
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
pub struct AccountCreationResponseData{
|
||||
pid: i32
|
||||
}
|
||||
|
||||
#[post("/v1/api/people", data="<data>")]
|
||||
pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Option<Errors>>{
|
||||
let database = database.inner();
|
||||
|
||||
// its fine to crash here if we cant get the next pid as that is in my opinion a dead state
|
||||
// anyways as noone can register anymore, EVER
|
||||
|
||||
let pid = next_pid(database).await;
|
||||
|
||||
let AccountCreationData {
|
||||
user_id,
|
||||
password,
|
||||
birth_date,
|
||||
tz_name,
|
||||
language,
|
||||
email: Email{
|
||||
address
|
||||
},
|
||||
mii: Mii{
|
||||
data,
|
||||
..
|
||||
},
|
||||
marketing_flag,
|
||||
gender,
|
||||
region,
|
||||
country,
|
||||
off_device_flag,
|
||||
..
|
||||
} = data.0;
|
||||
|
||||
|
||||
let password = generate_password(pid, &password).ok_or(None)?;
|
||||
|
||||
sqlx::query!("
|
||||
INSERT INTO users (
|
||||
pid,
|
||||
username,
|
||||
password,
|
||||
birthdate,
|
||||
timezone,
|
||||
email,
|
||||
country,
|
||||
language,
|
||||
marketing_allowed,
|
||||
off_device_allowed,
|
||||
region,
|
||||
gender,
|
||||
mii_data
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13
|
||||
)
|
||||
",
|
||||
pid,
|
||||
user_id.as_ref(),
|
||||
password,
|
||||
birth_date,
|
||||
tz_name.as_ref(),
|
||||
address.as_ref(),
|
||||
country.as_ref(),
|
||||
language.as_ref(),
|
||||
marketing_flag.0,
|
||||
off_device_flag.0,
|
||||
region,
|
||||
gender.as_ref(),
|
||||
data.as_ref()
|
||||
).execute(database).await.unwrap();
|
||||
|
||||
generate_s3_images(pid, &data).await;
|
||||
|
||||
Ok(
|
||||
Xml(AccountCreationResponseData{
|
||||
pid
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DevAttr{
|
||||
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EmailInfoOwnProfileData{
|
||||
address: String,
|
||||
id: u32,
|
||||
parent: YesNoVal,
|
||||
primary: YesNoVal,
|
||||
reachable: YesNoVal,
|
||||
#[serde(rename = "type")]
|
||||
email_type: String,
|
||||
updated_by: String,
|
||||
validated: YesNoVal,
|
||||
validated_date: Option<NaiveDateTime>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MiiImage{
|
||||
cached_url: String,
|
||||
id: u32,
|
||||
url: String,
|
||||
#[serde(rename = "type")]
|
||||
image_type: String
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MiiImages{
|
||||
mii_image: MiiImage
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MiiDataOwnProfileData{
|
||||
status: String,
|
||||
data: String,
|
||||
id: u32,
|
||||
mii_hash: String,
|
||||
mii_images: MiiImages,
|
||||
name: String,
|
||||
primary: YesNoVal
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
struct GetOwnProfileData{
|
||||
active_flag: YesNoVal,
|
||||
birth_date: NaiveDate,
|
||||
country: String,
|
||||
create_date: NaiveDateTime,
|
||||
gender: String,
|
||||
language: String,
|
||||
updated: NaiveDateTime,
|
||||
marketing_flag: YesNoVal,
|
||||
off_device_flag: YesNoVal,
|
||||
pid: i32,
|
||||
email: EmailInfoOwnProfileData,
|
||||
mii: MiiDataOwnProfileData,
|
||||
region: i32,
|
||||
tz_name: String,
|
||||
user_id: String,
|
||||
utc_offset: String
|
||||
}
|
||||
|
||||
#[get("/v1/api/people/@me/profile")]
|
||||
pub fn get_own_profile(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>>{
|
||||
let User{
|
||||
username,
|
||||
pid,
|
||||
account_level,
|
||||
mii_data,
|
||||
gender,
|
||||
birthdate,
|
||||
country,
|
||||
creation_date,
|
||||
timezone,
|
||||
language,
|
||||
email,
|
||||
email_verified_since,
|
||||
updated,
|
||||
marketing_allowed,
|
||||
off_device_allowed,
|
||||
region,
|
||||
..
|
||||
} = user.into();
|
||||
|
||||
let timezone_offset = (&*OFFSET_FROM_TIMEZONE).get(&timezone).unwrap().to_owned();
|
||||
|
||||
// whenever we need an id or hash we just take the gxhash of the data cause i dont want data clutter
|
||||
// this both avoids the data we have to store as well as data clutter whilest keeping the ids
|
||||
// always the same
|
||||
|
||||
let mii_data = mii_data
|
||||
.replace("\n", "")
|
||||
.replace("\t", "")
|
||||
.replace("\r", "")
|
||||
.replace(" ", "");
|
||||
|
||||
Ds(Xml(
|
||||
GetOwnProfileData{
|
||||
active_flag: YesNoVal(true),
|
||||
pid,
|
||||
user_id: username,
|
||||
gender,
|
||||
birth_date: birthdate,
|
||||
country,
|
||||
create_date: creation_date,
|
||||
tz_name: timezone,
|
||||
language,
|
||||
updated,
|
||||
marketing_flag: YesNoVal(marketing_allowed),
|
||||
email: EmailInfoOwnProfileData{
|
||||
id: gxhash32(email.as_bytes(), 0),
|
||||
address: email,
|
||||
validated: YesNoVal(email_verified_since.is_some()),
|
||||
validated_date: email_verified_since,
|
||||
email_type: "DEFAULT".to_string(),
|
||||
updated_by: "USER".to_string(),
|
||||
reachable: YesNoVal(true),
|
||||
primary: YesNoVal(true),
|
||||
parent: YesNoVal(false),
|
||||
},
|
||||
mii: MiiDataOwnProfileData{
|
||||
id: gxhash32(mii_data.as_bytes(), 0),
|
||||
// the bitmask here is to avoid causing an too big number as we dont know if the
|
||||
// wii u uses a 64 bit int here
|
||||
mii_hash: hex::encode(bytemuck::bytes_of(
|
||||
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000))
|
||||
)),
|
||||
name: mii::MiiData::read(&mii_data)
|
||||
.map(|v| v.name).unwrap_or("INVALID".to_string()),
|
||||
primary: YesNoVal(true),
|
||||
data: mii_data,
|
||||
status: "COMPLETED".to_string(),
|
||||
mii_images: MiiImages{
|
||||
mii_image: {
|
||||
let image_url = get_mii_img_url(pid, "tga");
|
||||
let url_hash = gxhash32(image_url.as_bytes(), 0);
|
||||
MiiImage {
|
||||
image_type: "standard".to_string(),
|
||||
id: url_hash,
|
||||
url: image_url.clone(),
|
||||
cached_url: image_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
off_device_flag: YesNoVal(off_device_allowed),
|
||||
region,
|
||||
utc_offset: timezone_offset,
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
#[put("/v1/api/people/@me/miis/@primary")]
|
||||
pub fn change_mii() {
|
||||
// stubbed(tecnically requires auth but this doesnt do anything so theres no harm in not doing auth here rn)
|
||||
}
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ pub struct Timezones<'a>{
|
|||
pub timezone: &'a [Timezone],
|
||||
}
|
||||
|
||||
pub static TIMEZONES: Lazy<HashMap<String, HashMap<String, Vec<Timezone>>>> = Lazy::new(||{
|
||||
pub static ZONE_TO_TIMEZONES: Lazy<HashMap<String, HashMap<String, Vec<Timezone>>>> = Lazy::new(||{
|
||||
let path = {
|
||||
// if this crashes then something is wrong with the server setup so crashing here is fine imo
|
||||
let mut path = env::current_dir().unwrap();
|
||||
|
|
@ -35,22 +35,36 @@ pub static TIMEZONES: Lazy<HashMap<String, HashMap<String, Vec<Timezone>>>> = La
|
|||
serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap()
|
||||
});
|
||||
|
||||
pub static OFFSET_FROM_TIMEZONE: Lazy<HashMap<String, String>> = Lazy::new(||{
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for val in ZONE_TO_TIMEZONES.values(){
|
||||
for val in val.values(){
|
||||
for tz in val{
|
||||
map.insert(tz.area.clone(), tz.utc_offset.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
});
|
||||
|
||||
|
||||
#[get("/v1/api/content/time_zones/<zone>/<lang>")]
|
||||
pub fn get_timezone(zone: &str, lang: &str) -> Option<Xml<Timezones<'static>>>{
|
||||
let timezone = (&*TIMEZONES).get(zone)?.get(lang)?;
|
||||
let timezone = (&*ZONE_TO_TIMEZONES).get(zone)?.get(lang)?;
|
||||
let timezones = Timezones{ timezone };
|
||||
Some(Xml(timezones))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use crate::nnid::timezones::{Timezones, TIMEZONES};
|
||||
use crate::nnid::timezones::{Timezones, ZONE_TO_TIMEZONES};
|
||||
use crate::xml::serialize_with_version;
|
||||
|
||||
#[test]
|
||||
fn test(){
|
||||
let timezone = (&*TIMEZONES).get("DE").unwrap().get("en").unwrap();
|
||||
let timezone = (&*ZONE_TO_TIMEZONES).get("DE").unwrap().get("en").unwrap();
|
||||
let timezones = Timezones{ timezone };
|
||||
let ser = serialize_with_version(&timezones).unwrap();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue