diff --git a/Cargo.lock b/Cargo.lock index 31fa8ea..5cc2276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,7 @@ dependencies = [ "prost", "quick-xml", "rand", + "reqwest", "rocket", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 7252c2d..fb981ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ tonic = "0.12.3" prost = "0.13.4" lettre = "0.11.15" rand = "0.8.5" +reqwest = "0.12.12" diff --git a/src/main.rs b/src/main.rs index acaab31..450e206 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,13 @@ use std::env; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use dotenvy::dotenv; use juniper::{EmptyMutation, EmptySubscription}; +use minio::s3::ClientBuilder; +use minio::s3::creds::StaticProvider; +use minio::s3::http::BaseUrl; +use once_cell::sync::Lazy; use rocket::fairing::AdHoc; use rocket::http::{ContentType, Header, Status}; use rocket::{catch, catchers, routes, Request}; @@ -11,6 +16,7 @@ use sqlx::Postgres; use sqlx::postgres::PgPoolOptions; use tonic::transport::Server; use crate::graphql::{Query, Schema}; +use crate::nnid::people::S3ClientState; mod xml; mod conntest; @@ -96,8 +102,32 @@ async fn launch() -> _ { .connect(&act_database_url).await .expect("unable to create pool"); + pub static S3_URL_STRING: Lazy> = Lazy::new(|| + env::var("S3_URL").expect("S3_URL not specified").into_boxed_str() + ); + + pub static S3_URL: Lazy = Lazy::new(|| + S3_URL_STRING.parse().unwrap() + ); + + pub static S3_USER: Lazy> = Lazy::new(|| + env::var("S3_USER").expect("S3_USER not specified").into_boxed_str() + ); + + pub static S3_PASSWD: Lazy> = Lazy::new(|| + env::var("S3_PASSWD").expect("S3_PASSWD not specified").into_boxed_str() + ); + + let s3_client = ClientBuilder::new(S3_URL.clone()) + .provider(Some(Box::new(StaticProvider::new(&S3_USER, &S3_PASSWD, None)))) + .build() + .expect("failed to create s3 client"); + rocket::build() .manage(pool) + .manage(S3ClientState { + client: Arc::new(s3_client), + }) .manage(graphql::Context(graph_pool)) .manage(Schema::new( Query, diff --git a/src/nnid/people.rs b/src/nnid/people.rs index c84910a..9de450d 100644 --- a/src/nnid/people.rs +++ b/src/nnid/people.rs @@ -1,4 +1,8 @@ use std::env; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::path::Path; use chrono::{NaiveDate, NaiveDateTime}; use gxhash::{gxhash32, gxhash64}; use minio::s3::builders::{ObjectContent}; @@ -10,31 +14,47 @@ 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::error::{Error, Errors}; use crate::nnid::pid_distribution::next_pid; use crate::nnid::timezones::{OFFSET_FROM_TIMEZONE}; use crate::Pool; use crate::xml::{Xml, YesNoVal}; use crate::email::send_verification_email; use rand::Rng; +use mii::{get_image_png, get_image_tga}; +use minio::s3::client::Client; +use minio::s3::args::PutObjectArgs; +use std::sync::Arc; -static S3_URL_STRING: Lazy> = Lazy::new(|| +const DATABASE_ERROR: Errors = Errors{ + error: &[ + Error{ + code: "9999", + message: "Internal server error" + } + ] +}; + +pub static S3_URL_STRING: Lazy> = Lazy::new(|| env::var("S3_URL").expect("S3_URL not specified").into_boxed_str() ); - -static S3_URL: Lazy = Lazy::new(|| +pub static S3_URL: Lazy = Lazy::new(|| S3_URL_STRING.parse().unwrap() ); -static S3_USER: Lazy> = Lazy::new(|| +pub static S3_USER: Lazy> = Lazy::new(|| env::var("S3_USER").expect("S3_USER not specified").into_boxed_str() ); -static S3_PASSWD: Lazy> = Lazy::new(|| +pub static S3_PASSWD: Lazy> = Lazy::new(|| env::var("S3_PASSWD").expect("S3_PASSWD not specified").into_boxed_str() ); +pub static S3_BUCKET: Lazy> = Lazy::new(|| + env::var("S3_BUCKET").expect("S3_BUCKET not specified").into_boxed_str() +); + fn get_mii_img_url_path(pid: i32, format: &str) -> String{ format!("mii/{}/main.{}", pid, format) } @@ -43,30 +63,44 @@ 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){ - - +pub 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 { + .build() + else { + println!("Failed to build S3 client for PID {}", pid); return; }; let Some(image) = mii::get_image_png(mii_data).await else { + println!("Failed to fetch PNG image for PID {}", pid); 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(); + + if let Err(e) = client.put_object_content(&**S3_BUCKET, &object_name, object_content).send().await { + println!("Failed to upload PNG for PID {}: {:?}", pid, e); + } else { + println!("Successfully uploaded PNG for PID {}", pid); + } let Some(image) = mii::get_image_tga(mii_data).await else { + println!("Failed to fetch TGA image for PID {}", pid); return; }; - let object_name = get_mii_img_url_path(pid, "tga"); + + 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(); + + if let Err(e) = client.put_object_content(&**S3_BUCKET, &object_name, object_content).send().await { + println!("Failed to upload TGA for PID {}: {:?}", pid, e); + } else { + println!("Successfully uploaded TGA for PID {}", pid); + } } #[derive(Deserialize)] @@ -74,6 +108,17 @@ pub struct Email{ address: Box } +pub struct S3ClientState { + pub client: Arc, +} + +#[derive(Deserialize)] +pub struct UpdateMiiData { + name: Box, + primary: crate::xml::YesNoVal, + data: Box, +} + #[derive(Deserialize, Serialize)] pub struct Mii{ name: Box, @@ -356,9 +401,111 @@ fn build_own_profile(user: User) -> Ds> { )) } +#[put("/v1/api/people/@me/miis/@primary", data = "")] +pub async fn change_mii( + database: &State, + s3: &State, + auth: Auth, + data: Xml, +) -> Result<(), Option>> { + let db = database.inner(); + let pid = auth.pid; + let mii_data = data.data.as_ref(); -#[put("/v1/api/people/@me/miis/@primary")] -pub fn change_mii() { - // stubbed(technically requires auth but this doesnt do anything so theres no harm in not doing auth here rn) + println!("Received new Mii data update for PID {}", pid); + + let result = sqlx::query!( + "UPDATE users SET mii_data = $1 WHERE pid = $2", + mii_data, + pid + ) + .execute(db) + .await; + + if let Err(e) = result { + println!("Failed to update Mii data for PID {}: {:?}", pid, e); + return Err(Some(DATABASE_ERROR)); + } + + println!("Successfully updated Mii data for PID {}", pid); + + generate_mii_images(s3.client.clone(), &**S3_BUCKET, pid, mii_data).await; + + Ok(()) } +pub async fn generate_mii_images(client: Arc, bucket: &str, pid: i32, mii_data: &str) { + let user_mii_key = format!("mii/{}", pid); + + async fn save_and_upload( + client: &Client, + bucket: &str, + key: &str, + data: &[u8], + ) -> Result<(), Box> { + let temp_path = format!("/tmp/{}", key.replace("/", "_")); + + { + let mut file = std::fs::File::create(&temp_path)?; + file.write_all(data)?; + file.flush()?; + } + + let content = ObjectContent::from(std::path::Path::new(&temp_path)); + client.put_object_content(bucket, key, content).send().await?; + + std::fs::remove_file(&temp_path)?; + + Ok(()) + } + + if let Some(png_data) = get_image_png(mii_data).await { + if let Err(e) = save_and_upload(&client, bucket, &format!("{}/normal_face.png", user_mii_key), &png_data).await { + println!("Failed to upload normal_face.png for PID {}: {:?}", pid, e); + } + } + + if let Some(tga_data) = get_image_tga(mii_data).await { + if let Err(e) = save_and_upload(&client, bucket, &format!("{}/standard.tga", user_mii_key), &tga_data).await { + println!("Failed to upload standard.tga for PID {}: {:?}", pid, e); + } + } + + let expressions = [ + "frustrated", + "smile_open_mouth", + "wink_left", + "sorrow", + "surprise_open_mouth", + ]; + + for expression in expressions.iter() { + let url = format!( + "https://mii-unsecure.ariankordi.net/miis/image.png?data={}&expression={}&type=face&width=128&instance_count=1", + mii_data, expression + ); + + if let Ok(resp) = reqwest::get(&url).await { + if let Ok(bytes) = resp.bytes().await { + if let Err(e) = save_and_upload(&client, bucket, &format!("{}/{}.png", user_mii_key, expression), &bytes).await { + println!("Failed to upload {}.png for PID {}: {:?}", expression, pid, e); + } + } + } + } + + let body_url = format!( + "https://mii-unsecure.ariankordi.net/miis/image.png?data={}&type=all_body&width=270&instance_count=1", + mii_data + ); + + if let Ok(resp) = reqwest::get(&body_url).await { + if let Ok(bytes) = resp.bytes().await { + if let Err(e) = save_and_upload(&client, bucket, &format!("{}/body.png", user_mii_key), &bytes).await { + println!("Failed to upload body.png for PID {}: {:?}", pid, e); + } + } + } + + println!("Finished Mii image generation for PID {}", pid); +} diff --git a/src/nnid/support.rs b/src/nnid/support.rs index 439aeac..d3e1a5d 100644 --- a/src/nnid/support.rs +++ b/src/nnid/support.rs @@ -20,9 +20,7 @@ pub struct ValidateEmailInput{ } #[post("/v1/api/support/validate/email", data="")] pub async fn validate(data: Form){ - if let Err(e) = send_verification_email(&data.email, 123456, "Andrea Test Username").await { - println!("Failed to send verification email: {e}"); - } + } #[put("/v1/api/support/email_confirmation//")]