Merge branch 'feat/mii' into 'main'
Mii routes See merge request perditum/account-rs!2
This commit is contained in:
commit
ee3a351259
5 changed files with 196 additions and 19 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -29,6 +29,7 @@ dependencies = [
|
||||||
"prost",
|
"prost",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rand",
|
"rand",
|
||||||
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ tonic = "0.12.3"
|
||||||
prost = "0.13.4"
|
prost = "0.13.4"
|
||||||
lettre = "0.11.15"
|
lettre = "0.11.15"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
reqwest = "0.12.12"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
30
src/main.rs
30
src/main.rs
|
|
@ -1,8 +1,13 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use juniper::{EmptyMutation, EmptySubscription};
|
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::fairing::AdHoc;
|
||||||
use rocket::http::{ContentType, Header, Status};
|
use rocket::http::{ContentType, Header, Status};
|
||||||
use rocket::{catch, catchers, routes, Request};
|
use rocket::{catch, catchers, routes, Request};
|
||||||
|
|
@ -11,6 +16,7 @@ use sqlx::Postgres;
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use tonic::transport::Server;
|
use tonic::transport::Server;
|
||||||
use crate::graphql::{Query, Schema};
|
use crate::graphql::{Query, Schema};
|
||||||
|
use crate::nnid::people::S3ClientState;
|
||||||
|
|
||||||
mod xml;
|
mod xml;
|
||||||
mod conntest;
|
mod conntest;
|
||||||
|
|
@ -96,8 +102,32 @@ async fn launch() -> _ {
|
||||||
.connect(&act_database_url).await
|
.connect(&act_database_url).await
|
||||||
.expect("unable to create pool");
|
.expect("unable to create pool");
|
||||||
|
|
||||||
|
pub static S3_URL_STRING: Lazy<Box<str>> = Lazy::new(||
|
||||||
|
env::var("S3_URL").expect("S3_URL not specified").into_boxed_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
pub static S3_URL: Lazy<BaseUrl> = Lazy::new(||
|
||||||
|
S3_URL_STRING.parse().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
pub static S3_USER: Lazy<Box<str>> = Lazy::new(||
|
||||||
|
env::var("S3_USER").expect("S3_USER not specified").into_boxed_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
pub static S3_PASSWD: Lazy<Box<str>> = 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()
|
rocket::build()
|
||||||
.manage(pool)
|
.manage(pool)
|
||||||
|
.manage(S3ClientState {
|
||||||
|
client: Arc::new(s3_client),
|
||||||
|
})
|
||||||
.manage(graphql::Context(graph_pool))
|
.manage(graphql::Context(graph_pool))
|
||||||
.manage(Schema::new(
|
.manage(Schema::new(
|
||||||
Query,
|
Query,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
use gxhash::{gxhash32, gxhash64};
|
use gxhash::{gxhash32, gxhash64};
|
||||||
use minio::s3::builders::{ObjectContent};
|
use minio::s3::builders::{ObjectContent};
|
||||||
|
|
@ -10,31 +14,47 @@ use rocket::{get, post, put, State};
|
||||||
use rocket::serde::{Deserialize, Serialize};
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
use crate::account::account::{generate_password, Auth, User};
|
use crate::account::account::{generate_password, Auth, User};
|
||||||
use crate::dsresponse::Ds;
|
use crate::dsresponse::Ds;
|
||||||
use crate::error::Errors;
|
use crate::error::{Error, Errors};
|
||||||
use crate::nnid::pid_distribution::next_pid;
|
use crate::nnid::pid_distribution::next_pid;
|
||||||
use crate::nnid::timezones::{OFFSET_FROM_TIMEZONE};
|
use crate::nnid::timezones::{OFFSET_FROM_TIMEZONE};
|
||||||
use crate::Pool;
|
use crate::Pool;
|
||||||
use crate::xml::{Xml, YesNoVal};
|
use crate::xml::{Xml, YesNoVal};
|
||||||
use crate::email::send_verification_email;
|
use crate::email::send_verification_email;
|
||||||
use rand::Rng;
|
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<Box<str>> = Lazy::new(||
|
const DATABASE_ERROR: Errors = Errors{
|
||||||
|
error: &[
|
||||||
|
Error{
|
||||||
|
code: "9999",
|
||||||
|
message: "Internal server error"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static S3_URL_STRING: Lazy<Box<str>> = Lazy::new(||
|
||||||
env::var("S3_URL").expect("S3_URL not specified").into_boxed_str()
|
env::var("S3_URL").expect("S3_URL not specified").into_boxed_str()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
pub static S3_URL: Lazy<BaseUrl> = Lazy::new(||
|
||||||
static S3_URL: Lazy<BaseUrl> = Lazy::new(||
|
|
||||||
S3_URL_STRING.parse().unwrap()
|
S3_URL_STRING.parse().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
static S3_USER: Lazy<Box<str>> = Lazy::new(||
|
pub static S3_USER: Lazy<Box<str>> = Lazy::new(||
|
||||||
env::var("S3_USER").expect("S3_USER not specified").into_boxed_str()
|
env::var("S3_USER").expect("S3_USER not specified").into_boxed_str()
|
||||||
);
|
);
|
||||||
|
|
||||||
static S3_PASSWD: Lazy<Box<str>> = Lazy::new(||
|
pub static S3_PASSWD: Lazy<Box<str>> = Lazy::new(||
|
||||||
env::var("S3_PASSWD").expect("S3_PASSWD not specified").into_boxed_str()
|
env::var("S3_PASSWD").expect("S3_PASSWD not specified").into_boxed_str()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
pub static S3_BUCKET: Lazy<Box<str>> = 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{
|
fn get_mii_img_url_path(pid: i32, format: &str) -> String{
|
||||||
format!("mii/{}/main.{}", pid, format)
|
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))
|
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 auth = StaticProvider::new(&S3_USER, &S3_PASSWD, None);
|
||||||
|
|
||||||
let Ok(client) = ClientBuilder::new(S3_URL.clone())
|
let Ok(client) = ClientBuilder::new(S3_URL.clone())
|
||||||
.provider(Some(Box::new(auth)))
|
.provider(Some(Box::new(auth)))
|
||||||
.build() else {
|
.build()
|
||||||
|
else {
|
||||||
|
println!("Failed to build S3 client for PID {}", pid);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(image) = mii::get_image_png(mii_data).await else {
|
let Some(image) = mii::get_image_png(mii_data).await else {
|
||||||
|
println!("Failed to fetch PNG image for PID {}", pid);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let object_name = get_mii_img_url_path(pid, "png");
|
let object_name = get_mii_img_url_path(pid, "png");
|
||||||
let object_content = ObjectContent::from(image);
|
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 {
|
let Some(image) = mii::get_image_tga(mii_data).await else {
|
||||||
|
println!("Failed to fetch TGA image for PID {}", pid);
|
||||||
return;
|
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);
|
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)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -74,6 +108,17 @@ pub struct Email{
|
||||||
address: Box<str>
|
address: Box<str>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct S3ClientState {
|
||||||
|
pub client: Arc<Client>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateMiiData {
|
||||||
|
name: Box<str>,
|
||||||
|
primary: crate::xml::YesNoVal,
|
||||||
|
data: Box<str>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Mii{
|
pub struct Mii{
|
||||||
name: Box<str>,
|
name: Box<str>,
|
||||||
|
|
@ -356,9 +401,111 @@ fn build_own_profile(user: User) -> Ds<Xml<GetOwnProfileData>> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[put("/v1/api/people/@me/miis/@primary", data = "<data>")]
|
||||||
|
pub async fn change_mii(
|
||||||
|
database: &State<Pool>,
|
||||||
|
s3: &State<S3ClientState>,
|
||||||
|
auth: Auth<false>,
|
||||||
|
data: Xml<UpdateMiiData>,
|
||||||
|
) -> Result<(), Option<Errors<'static>>> {
|
||||||
|
let db = database.inner();
|
||||||
|
let pid = auth.pid;
|
||||||
|
let mii_data = data.data.as_ref();
|
||||||
|
|
||||||
#[put("/v1/api/people/@me/miis/@primary")]
|
println!("Received new Mii data update for PID {}", pid);
|
||||||
pub fn change_mii() {
|
|
||||||
// stubbed(technically requires auth but this doesnt do anything so theres no harm in not doing auth here rn)
|
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<Client>, 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<dyn std::error::Error>> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@ pub struct ValidateEmailInput{
|
||||||
}
|
}
|
||||||
#[post("/v1/api/support/validate/email", data="<data>")]
|
#[post("/v1/api/support/validate/email", data="<data>")]
|
||||||
pub async fn validate(data: Form<ValidateEmailInput>){
|
pub async fn validate(data: Form<ValidateEmailInput>){
|
||||||
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/<pid>/<code>")]
|
#[put("/v1/api/support/email_confirmation/<pid>/<code>")]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue