feat: stuff

This commit is contained in:
DJMrTV 2025-03-05 20:28:25 +01:00
commit a40b1498e2
16 changed files with 2992 additions and 256 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[target.'cfg(target_arch = "x86_64")']
rustflags = ["-C", "target-feature=+aes,+sse2"]

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
/target
target
.idea
.env

756
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,9 @@
[package]
name = "account"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
rocket = "0.5.1"
@ -15,10 +17,17 @@ serde_json = "1.0.139"
chrono = { version = "0.4.39", features = ["serde"] }
argon2 = "0.5.3"
sha2 = "0.10.8"
bytemuck = "1.21.0"
bytemuck = { version = "1.21.0", features = ["derive"] }
base64 = "0.22.1"
hex = "0.4.3"
thiserror = "2.0.11"
bcrypt = "0.17.0"
sqlx = { version = "0.8.3", features = [ "runtime-tokio", "tls-native-tls", "postgres", "chrono" ] }
aes = "0.8.4"
hmac = "0.12.1"
md-5 = "0.10.6"
cbc = "0.1.2"
mii = { path = "./mii" }
minio = { git = "https://github.com/minio/minio-rs.git" }
crc32fast = "1.4.2"
gxhash = "3.4.1"

1539
mii/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

12
mii/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "mii"
version = "0.1.0"
edition = "2024"
[dependencies]
base64 = "0.22.1"
bytemuck = { version = "1.21.0", features = ["derive"] }
reqwest = "0.12.12"
[dev-dependencies]
tokio = { version = "1.43.0", features = ["macros"] }

84
mii/src/lib.rs Normal file
View file

@ -0,0 +1,84 @@
use std::ops::Index;
use std::str::FromStr;
use bytemuck::{from_bytes, try_from_bytes, Pod, Zeroable};
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use reqwest::Url;
#[derive(Pod, Zeroable, Copy, Clone)]
#[repr(C, packed)]
struct FFLStoreData{
mii_data: FFLiMiiDataOfficial
}
#[derive(Pod, Zeroable, Copy, Clone)]
#[repr(C, packed)]
struct FFLiMiiDataOfficial{
core_data: FFLiMiiDataCore
}
#[derive(Pod, Zeroable, Copy, Clone)]
#[repr(C, packed)]
struct FFLiMiiDataCore{
stuff: u32,
author_id: u64,
create_id: [u8; 10],
unk_1: u16,
unk_2: u16,
pub name: [u16; 10],
}
pub struct MiiData{
pub name: String
}
impl MiiData{
pub fn read(data: &str) -> Option<Self>{
let data = BASE64_STANDARD.decode(data).ok()?;
let data: &FFLStoreData = try_from_bytes(data.get(0..size_of::<FFLStoreData>())?).ok()?;
let name = data.mii_data.core_data.name;
let idx = name.iter().position(|v| *v == 0x0).unwrap_or(11);
let name = &name[0..idx];
let name = String::from_utf16(&name).ok()?;
Some(Self{
name
})
}
}
pub async fn get_image_png(data: &str) -> Option<Vec<u8>>{
let mut url = Url::from_str("https://mii-unsecure.ariankordi.net/miis/image.png\
").unwrap();
url.set_query(Some(&format!("data={}", data)));
reqwest::get(url).await.ok().map(|v| v.bytes())?.await.ok().map(|b| b.to_vec())
}
pub async fn get_image_tga(data: &str) -> Option<Vec<u8>>{
let mut url = Url::from_str("https://mii-unsecure.ariankordi.net/miis/image.tga\
").unwrap();
url.set_query(Some(&format!("data={}", data)));
reqwest::get(url).await.ok().map(|v| v.bytes())?.await.ok().map(|b| b.to_vec())
}
#[cfg(test)]
mod test{
#[tokio::test]
async fn test_image_get(){
let image = get_image_png("AAEAQDrPvmeBxJIQ3cL/BYp4iCWDvgAA8FVEAEoATQByAFQAVgAAAGgAZQByAAB/BAApBBpK4xiXEqQMAhgXbAoACClQQkhQTQBFAAAALQBTAHcAaQB0AGMAaAAAAMqP").await.unwrap();
fs::write("heh.png", image).unwrap();
}
}

View file

@ -1,11 +1,12 @@
use std::io::Write;
use std::ops::{Deref, DerefMut};
use argon2::{Algorithm, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::SaltString;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use bytemuck::bytes_of;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
use log::{error, warn};
use rocket::http::Status;
use rocket::{async_trait, Request};
@ -13,6 +14,7 @@ use rocket::request::{FromRequest, Outcome};
use sha2::{Digest, Sha256};
use sha2::digest::FixedOutput;
use crate::error::{Error, Errors};
use crate::nnid::oauth::TokenData;
use crate::Pool;
macro_rules! request_try {
@ -49,7 +51,9 @@ pub struct User {
pub marketing_allowed: bool,
pub off_device_allowed: bool,
pub region: i32,
pub mii_data: String
pub mii_data: String,
pub creation_date: NaiveDateTime,
pub updated: NaiveDateTime
}
fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{
@ -120,15 +124,67 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<Use
}
async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<User> {
let data = BASE64_STANDARD.decode(&token).ok()?;
let data = TokenData::decode(token)?;
warn!("bearer token login currently unsupported");
let token_info =
sqlx::query!(
"select * from tokens where pid = $1 and token_id = $2 and random =$3",
data.pid, data.token_id, data.random
).
fetch_one(connection).await.ok()?;
None
if token_info.expires.and_utc() < Utc::now(){
return None
}
let mut user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE pid = $1",
token_info.pid
).fetch_one(connection).await.ok()?;
Some(user)
}
pub struct Auth<const FORCE_BEARER_AUTH: bool>(pub User);
impl<const FORCE_BEARER_AUTH: bool> AsRef<User> for Auth<FORCE_BEARER_AUTH>{
fn as_ref(&self) -> &User {
&self.0
}
}
impl<const FORCE_BEARER_AUTH: bool> AsMut<User> for Auth<FORCE_BEARER_AUTH>{
fn as_mut(&mut self) -> &mut User {
&mut self.0
}
}
impl<const FORCE_BEARER_AUTH: bool> Deref for Auth<FORCE_BEARER_AUTH>{
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<const FORCE_BEARER_AUTH: bool> DerefMut for Auth<FORCE_BEARER_AUTH>{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<const FORCE_BEARER_AUTH: bool> Into<User> for Auth<FORCE_BEARER_AUTH>{
fn into(self) -> User {
self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for User{
impl<'r, const FORCE_BEARER_AUTH: bool> FromRequest<'r> for Auth<FORCE_BEARER_AUTH>{
type Error = Errors<'static>;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
@ -139,7 +195,7 @@ impl<'r> FromRequest<'r> for User{
let (auth_type, token) = request_try!(auth.split_once(' ').ok_or(INVALID_TOKEN_ERRORS));
let user = match auth_type{
"Basic" => read_basic_auth_token(pool, token).await,
"Basic" if !FORCE_BEARER_AUTH => read_basic_auth_token(pool, token).await,
"Bearer" => read_bearer_auth_token(pool, token).await,
_ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
};
@ -148,6 +204,6 @@ impl<'r> FromRequest<'r> for User{
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
};
Outcome::Success(user)
Outcome::Success(Self(user))
}
}

View file

@ -1,5 +1 @@
use serde::{Deserialize, Serialize};
trait DataWrapper{
}
use serde::{Deserialize, Serialize};

View file

@ -1,3 +1,5 @@
use std::env;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
@ -57,7 +59,8 @@ async fn launch() -> _ {
nnid::timezones::get_timezone,
nnid::person_exists::person_exists,
nnid::email::validate,
nnid::create_account::create_account,
nnid::people::create_account,
nnid::people::get_own_profile,
nnid::oauth::generate_token::generate_token
])
}

View file

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

View file

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

View file

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

View file

@ -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
View 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)
}

View file

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