use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::database::DbContext; #[derive(sqlx::FromRow, Deserialize, Serialize)] pub struct PgUser { pub id: i32, pub username: String, pub country_code: String, pub country_name: String, pub cover_url: String, pub avatar_url: String, pub pp: f32, pub global_rank: Option, pub country_rank: Option, pub total_score: i64, pub ranked_score: i64, pub hit_accuracy: f32, pub play_count: i32, pub level: i32, pub level_progress: i32, pub last_refreshed: DateTime, } impl DbContext { pub async fn set_user(&mut self, user: PgUser) -> Result { let pool = match self.get_pg_connection().await { Some(x) => x, None => return Err(()), }; let PgUser { id, username, country_code, country_name, cover_url, avatar_url, pp, global_rank, country_rank, total_score, ranked_score, hit_accuracy, play_count, level, level_progress, last_refreshed, } = user.into(); let user_chk: Option = match sqlx::query_as("SELECT * FROM public.user WHERE id = $1") .bind(&id) .fetch_optional(pool) .await { Ok(x) => x, Err(e) => { log::error!("Failed to check for existing users: {e}"); return Err(()); } }; if user_chk.is_none() { match sqlx::query_as("INSERT INTO public.user (id, username, country_code, country_name, cover_url, avatar_url, pp, global_rank, country_rank, total_score, ranked_score, hit_accuracy, play_count, level, level_progress, last_refreshed) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING *;") .bind(&id) .bind(&username) .bind(&country_code) .bind(&country_name) .bind(&cover_url) .bind(&avatar_url) .bind(&pp) .bind(&global_rank) .bind(&country_rank) .bind(&total_score) .bind(&ranked_score) .bind(&hit_accuracy) .bind(&play_count) .bind(&level) .bind(&level_progress) .bind(&last_refreshed) .fetch_one(pool) .await { Ok(x) => Ok(x), Err(e) => { log::error!("Failed to insert user object: {e}"); Err(()) } } } else { match sqlx::query_as("UPDATE public.user SET id = $1, username = $2, country_code = $3, country_name = $4, cover_url = $5, avatar_url = $6, pp = $7, global_rank = $8, country_rank = $9, total_score = $10, ranked_score = $11, hit_accuracy = $12, play_count = $13, level = $14, level_progress = $15, last_refreshed = $16 WHERE id = $1") .bind(&id) .bind(&username) .bind(&country_code) .bind(&country_name) .bind(&cover_url) .bind(&avatar_url) .bind(&pp) .bind(&global_rank) .bind(&country_rank) .bind(&total_score) .bind(&ranked_score) .bind(&hit_accuracy) .bind(&play_count) .bind(&level) .bind(&level_progress) .bind(&last_refreshed) .fetch_one(pool) .await { Ok(x) => Ok(x), Err(e) => { log::error!("Failed to insert user object: {e}"); Err(()) } } } } } impl TryFrom for PgUser { type Error = TypeConversionError; fn try_from(value: bingolib::structs::User) -> Result { let bingolib::structs::User { id, username, country_code, country_name, cover_url, avatar_url, pp, global_rank, country_rank, total_score, ranked_score, hit_accuracy, play_count, level, level_progress, last_refreshed, } = value; let user = PgUser { id: u32_to_i32(id, "id")?, username, country_code, country_name, cover_url, avatar_url, pp, global_rank: global_rank .map(|x| u32_to_i32(x, "global_rank")) .transpose()?, country_rank: country_rank .map(|x| u32_to_i32(x, "country_rank")) .transpose()?, total_score: u64_to_i64(total_score, "total_score")?, ranked_score: u64_to_i64(ranked_score, "ranked_score")?, hit_accuracy, play_count: u32_to_i32(play_count, "play_count")?, level: u32_to_i32(level, "level")?, level_progress: u32_to_i32(level_progress, "level_progress")?, last_refreshed, }; Ok(user) } } #[derive(Debug)] pub struct TypeConversionError { pub property: String, } fn u32_to_i32(x: u32, property: &str) -> Result { match i32::try_from(x) { Ok(x) => Ok(x), Err(_) => Err(TypeConversionError { property: property.into(), }), } } fn u64_to_i64(x: u64, property: &str) -> Result { match i64::try_from(x) { Ok(x) => Ok(x), Err(_) => Err(TypeConversionError { property: property.into(), }), } }