Initial
This commit is contained in:
		
						commit
						56a616fcd2
					
				|  | @ -0,0 +1,2 @@ | |||
| /target | ||||
| .env | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,5 @@ | |||
| [workspace] | ||||
| members = [ | ||||
| 	"server", | ||||
| 	"front", | ||||
| ] | ||||
|  | @ -0,0 +1,8 @@ | |||
| [package] | ||||
| name = "front" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| 
 | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| 
 | ||||
| [dependencies] | ||||
|  | @ -0,0 +1,3 @@ | |||
| fn main() { | ||||
|     println!("Hello, world!"); | ||||
| } | ||||
|  | @ -0,0 +1,2 @@ | |||
| [target.aarch64-unknown-linux-gnu] | ||||
| linker = "aarch64-linux-gnu-gcc" | ||||
|  | @ -0,0 +1,18 @@ | |||
| [package] | ||||
| name = "server" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["Emilia Allison"] | ||||
| 
 | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| 
 | ||||
| [dependencies] | ||||
| actix-web = "4" | ||||
| dotenvy = "0.15" | ||||
| env_logger = "0.10" | ||||
| log = "0.4" | ||||
| serde = { version = "1.0", features = ["derive"]} | ||||
| serde_json = "1.0" | ||||
| sqlx = { version = "0.6.2", features = ["runtime-actix-native-tls", "mysql", "time"]} | ||||
| reqwest = { version = "0.11", features = ["json"] } | ||||
| openssl = { version = "0.10", features = ["vendored"] } | ||||
|  | @ -0,0 +1,26 @@ | |||
| use crate::sql_users::GetRefreshTokenByIdErr; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum JointErr { | ||||
|     SqlxError(sqlx::Error), | ||||
|     ReqwestError(reqwest::Error), | ||||
|     Empty, | ||||
| } | ||||
| impl From<sqlx::Error> for JointErr { | ||||
|     fn from(value: sqlx::Error) -> Self { | ||||
|         JointErr::SqlxError(value) | ||||
|     } | ||||
| } | ||||
| impl From<reqwest::Error> for JointErr { | ||||
|     fn from(value: reqwest::Error) -> Self { | ||||
|         JointErr::ReqwestError(value) | ||||
|     } | ||||
| } | ||||
| impl From<GetRefreshTokenByIdErr> for JointErr { | ||||
|     fn from(value: GetRefreshTokenByIdErr) -> Self { | ||||
|         match value { | ||||
|             GetRefreshTokenByIdErr::SqlxError(e) => e.into(), | ||||
|             GetRefreshTokenByIdErr::Empty => JointErr::Empty, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,84 @@ | |||
| mod spotify_auth; | ||||
| mod spotify_get_top; | ||||
| mod spotify_playlist; | ||||
| mod spotify_types; | ||||
| 
 | ||||
| mod sql_users; | ||||
| mod sql_tracks; | ||||
| 
 | ||||
| mod state; | ||||
| mod joint_err; | ||||
| mod refresh; | ||||
| 
 | ||||
| use spotify_auth::callback_manager; | ||||
| use refresh::refresh_manager; | ||||
| 
 | ||||
| use actix_web::{App, HttpServer, HttpResponse, get, Responder, web}; | ||||
| use dotenvy::dotenv; | ||||
| 
 | ||||
| use std::env; | ||||
| 
 | ||||
| const BASE_URL: &'static str = "https://api.spotify.com/v1"; | ||||
| 
 | ||||
| #[get("/")] | ||||
| async fn root(data: web::Data<state::AppState>) -> impl Responder { | ||||
|     log::warn!("Access to root"); | ||||
|     HttpResponse::Ok().body(format!("hi, pls go to: {}\n k thx", data.auth_url)) | ||||
| } | ||||
| 
 | ||||
| #[get("/failed")] | ||||
| async fn failed() -> impl Responder { | ||||
|     HttpResponse::Ok().body("yeah sorry bud, something went wrong.") | ||||
| } | ||||
| 
 | ||||
| #[get("/cool")] | ||||
| async fn succeeded() -> impl Responder { | ||||
|     HttpResponse::Ok().body("whatever you were doing worked. bye") | ||||
| } | ||||
| 
 | ||||
| #[actix_web::main] | ||||
| async fn main() -> std::io::Result<()> { | ||||
|     env_logger::builder() | ||||
|         .target(env_logger::Target::Stdout) | ||||
|         .filter_level(log::LevelFilter::Warn) | ||||
|         .init(); | ||||
|     dotenv().ok(); | ||||
|     let port: u16 = std::env::var("PORT") | ||||
|         .expect("Cannot proceed without port") | ||||
|         .parse().expect("Port must be a number"); | ||||
|     let address = std::env::var("ADDRESS").expect("Cannot proceed without a port to listen to"); | ||||
| 
 | ||||
|     let db_url = env::var("DATABASE_URL").expect("Cannot proceed without database url"); | ||||
|     let pool = sqlx::mysql::MySqlPoolOptions::new() | ||||
|         .max_connections(5) | ||||
|         .connect(&db_url) | ||||
|         .await | ||||
|         .expect("Cannot proceed with failed database connection"); | ||||
| 
 | ||||
|     HttpServer::new(move || { | ||||
|         App::new() | ||||
|             .app_data(web::Data::new(state::AppState { | ||||
|                 auth_url: construct_auth_url(), | ||||
|                 pool: pool.clone(), | ||||
|             })) | ||||
|             .service(root) | ||||
|             .service(callback_manager) | ||||
|             .service(succeeded) | ||||
|             .service(failed) | ||||
|             .service(refresh_manager) | ||||
|     }) | ||||
|     .bind((address, port))? | ||||
|     .run() | ||||
|     .await | ||||
| } | ||||
| 
 | ||||
| fn construct_auth_url() -> String { | ||||
|     let redirect_url = env::var("REDIRECT_URL").expect("Cannot proceed without redirect url"); | ||||
|     let client_id = env::var("CLIENT_ID").expect("Cannot proceed without client id"); | ||||
| 
 | ||||
|     format!("https://accounts.spotify.com/authorize?client_id={}&scope={}&redirect_uri={}&response_type={}", | ||||
|         client_id, | ||||
|         "user-top-read playlist-modify-public user-read-email", | ||||
|         redirect_url, | ||||
|         "code") | ||||
| } | ||||
|  | @ -0,0 +1,74 @@ | |||
| use serde::Deserialize; | ||||
| use actix_web::web::Redirect; | ||||
| use actix_web::{HttpResponse, get, Responder, web}; | ||||
| use sqlx::MySqlPool; | ||||
| 
 | ||||
| use crate::spotify_get_top::get_top_tracks; | ||||
| use crate::{sql_users, spotify_auth, sql_tracks, spotify_playlist}; | ||||
| use crate::joint_err::JointErr; | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct RefreshData { | ||||
|     spotify_id: Option<String>, | ||||
|     force: Option<bool>, | ||||
| } | ||||
| 
 | ||||
| #[get("/refresh")] | ||||
| pub async fn refresh_manager( | ||||
|     data: web::Query<RefreshData>, | ||||
|     state: web::Data<crate::state::AppState> | ||||
|     ) -> impl Responder 
 | ||||
| { | ||||
|     let pool = &state.pool; | ||||
| 
 | ||||
|     if let Some(spotify_id) = &data.spotify_id { | ||||
|         let should_refresh = sql_users::get_refreshed_by_id(spotify_id, pool) | ||||
|             .await | ||||
|             .map(|time| sqlx::types::time::OffsetDateTime::now_utc() - time) | ||||
|             .map(|since| since.whole_hours().abs() >= 6) | ||||
|             .unwrap_or(false); | ||||
|         if should_refresh || data.force.unwrap_or(false) { | ||||
|             let e = refresh(spotify_id, pool).await; | ||||
|             log::error!("Refresh res was:\n{:?}", e); | ||||
|             HttpResponse::Ok().body("refreshing") | ||||
|         } else { | ||||
|             HttpResponse::TooManyRequests().body("it's been less than 6 hours. nah.") | ||||
|         } | ||||
|     } else { | ||||
|         HttpResponse::BadRequest().body("there's no ID here idiot") | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async fn refresh(spotify_id: &str, pool: &MySqlPool) -> Result<(), JointErr> { | ||||
|     let token = spotify_auth::refresh_token_for_id(spotify_id, pool).await?; | ||||
|     let internal_id = sql_users::get_internal_id_by_id(spotify_id, pool).await?; | ||||
|     let top: Vec<_> = get_top_tracks(&token).await?.items | ||||
|         .iter() | ||||
|         .map(|track| track.uri.clone()) | ||||
|         .collect(); | ||||
|     for track in top { | ||||
|         log::info!("Trying to insert {}", track.0); | ||||
|         sqlx::query!(" | ||||
|             INSERT INTO Tracks (UserId, Uri, DateAdded) | ||||
|             VALUES (?, ?, CURDATE()) | ||||
|             ON DUPLICATE KEY UPDATE DateAdded = CURDATE() | ||||
|             ", internal_id, track.0)
 | ||||
|             .execute(pool) | ||||
|             .await?; | ||||
|     } | ||||
| 
 | ||||
|     // Reset playlist
 | ||||
|     set_playlist("6Zk0tvx4q6Zf5AZglvRLBT", pool).await?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| async fn set_playlist(id: &str, pool: &MySqlPool) -> Result<(), JointErr> { | ||||
|     let tracklist = sql_tracks::even_allocation(pool).await?; | ||||
|     let emilia_id = sql_users::get_emilia_spotify_id(pool).await?; | ||||
|     let token = spotify_auth::refresh_token_for_id(&emilia_id, pool).await?; | ||||
| 
 | ||||
|     spotify_playlist::set_playlist(id, tracklist, &token, pool).await?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
|  | @ -0,0 +1,148 @@ | |||
| use actix_web::web::Redirect; | ||||
| use actix_web::{HttpResponse, get, Responder, web}; | ||||
| use serde::Deserialize; | ||||
| use serde_json::json; | ||||
| use sqlx::MySqlPool; | ||||
| use sqlx::mysql::MySqlPoolOptions; | ||||
| use std::{env, collections::HashMap}; | ||||
| 
 | ||||
| use crate::spotify_get_top::get_top_tracks; | ||||
| use crate::spotify_playlist::add_to_playlist; | ||||
| use crate::spotify_types::*; | ||||
| use crate::joint_err::JointErr; | ||||
| 
 | ||||
| use crate::sql_users::{add_user_dont_care, get_refresh_token_by_id, GetRefreshTokenByIdErr, AddUserSuccess}; | ||||
| 
 | ||||
| const URL: &'static str = "https://accounts.spotify.com/api/token"; | ||||
| 
 | ||||
| fn read_from_env() -> (String, String, String) { | ||||
|     let id = env::var("CLIENT_ID").expect("Cannot proceed without client id"); | ||||
|     let secret = env::var("CLIENT_SECRET").expect("Cannot proceed without client secret"); | ||||
|     let url = env::var("REDIRECT_URL").expect("Cannot proceed without redirect url"); | ||||
|     return (id, secret, url) | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct CallbackData { | ||||
|     code: String, | ||||
| } | ||||
| 
 | ||||
| #[get("/callback")] | ||||
| pub async fn callback_manager(data: web::Query<CallbackData>, state: web::Data<crate::state::AppState>) -> impl Responder { | ||||
|     log::warn!("Access to /callback"); | ||||
|     let pool = &state.pool; | ||||
| 
 | ||||
|     let tokens = get_auth_token(&data.code).await; | ||||
|     if let Ok(tokens) = tokens { | ||||
|         log::info!("{:?}", tokens.refresh_token); | ||||
|         let user_profile = get_user_email(&tokens.access_token).await; | ||||
|         if let Ok(up) = user_profile { | ||||
|             let q = add_user_dont_care(&up.email, &up.id, &tokens.refresh_token, &pool).await; | ||||
|             match q { | ||||
|                 Ok(AddUserSuccess::New) => { | ||||
|                     log::warn!("Added a new user: {}", up.id); | ||||
|                     Redirect::to(format!("/refresh?spotify_id={}&force=true", up.id)) | ||||
|                     }, | ||||
|                 Ok(AddUserSuccess::Duplicate) => { | ||||
|                     Redirect::to(format!("/refresh?spotify_id={}", up.id)) | ||||
|                 }, | ||||
|                 _ => { | ||||
|                     log::error!("SQL Query Failed: couldn't add new user\n{:?}", q); | ||||
|                     Redirect::to("/failed") | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             log::error!("We failed to get a user profile, the error was:\n{:?}", user_profile.unwrap_err()); | ||||
|             Redirect::to("/failed") | ||||
|         } | ||||
|     } else  { | ||||
|         log::error!("We failed to get tokens, the error was:\n{:?}", tokens.unwrap_err()); | ||||
|         Redirect::to("/failed") | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[non_exhaustive] | ||||
| struct AuthorizationRequestResponse { | ||||
|     access_token: Token, | ||||
|     refresh_token: Token, | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize, Debug)] | ||||
| pub struct Token(pub String); | ||||
| 
 | ||||
| async fn get_auth_token(code: &str) -> Result<AuthorizationRequestResponse, reqwest::Error> { | ||||
|     // Consider using env for REDIRECT_URI
 | ||||
|     //const REDIRECT_URI: &'static str = "https://ilia.moe/cool-stuff/cool-spotify-blend/callback";
 | ||||
|     const GRANT_TYPE: &'static str = "authorization_code"; | ||||
| 
 | ||||
|     let (id, secret, redirect_uri) = read_from_env(); | ||||
| 
 | ||||
|     let mut params = HashMap::new(); | ||||
|     params.insert("grant_type", GRANT_TYPE.to_owned()); | ||||
|     params.insert("code", code.to_owned()); | ||||
|     params.insert("redirect_uri", redirect_uri.to_owned()); | ||||
|     params.insert("client_id", id); | ||||
|     params.insert("client_secret", secret); | ||||
| 
 | ||||
|     let client = reqwest::Client::new(); | ||||
|     let res = client.post(URL.to_owned() ) | ||||
|         .query(¶ms) | ||||
|         .header("Content-Type", "application/x-www-form-urlencoded") | ||||
|         .header("Content-Length", 0) | ||||
|         .send() | ||||
|         .await? | ||||
|         .json() | ||||
|         .await?; | ||||
| 
 | ||||
|     Ok(res) | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| #[derive(Deserialize, Debug)] | ||||
| struct RefreshTokenForIdRes { | ||||
|     access_token: Token | ||||
| } | ||||
| 
 | ||||
| pub async fn refresh_token_for_id(spotify_id: &str, pool: &MySqlPool) -> Result<Token, JointErr> { | ||||
|     // Get the refresh token from the database:
 | ||||
|     let token = get_refresh_token_by_id(spotify_id, pool).await?; | ||||
|     let (id, secret, _) = read_from_env(); | ||||
| 
 | ||||
|     let mut params = HashMap::new(); | ||||
|     params.insert("grant_type", "refresh_token"); | ||||
|     params.insert("refresh_token", &token.0); | ||||
|     params.insert("client_id", &id); | ||||
|     params.insert("client_secret", &secret); | ||||
| 
 | ||||
|     let client = reqwest::Client::new(); | ||||
|     let res: RefreshTokenForIdRes = client.post(URL) | ||||
|         .header("Content-Type", "application/x-www-form-urlencoded") | ||||
|         .header("Content-Length", 0) | ||||
|         .query(¶ms) | ||||
|         .send() | ||||
|         .await? | ||||
|         .json() | ||||
|         .await?; | ||||
| 
 | ||||
|     Ok(res.access_token) | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Deserialize)] | ||||
| struct UserProfile { | ||||
|     pub email: String, | ||||
|     pub id: String, | ||||
| } | ||||
| 
 | ||||
| async fn get_user_email(token: &Token) -> Result<UserProfile, reqwest::Error> { | ||||
|     let client = reqwest::Client::new(); | ||||
|     let res = client.get(crate::BASE_URL.to_owned() + "/me") | ||||
|         .header("Authorization", "Bearer ".to_owned() + &token.0) | ||||
|         .send() | ||||
|         .await? | ||||
|         .json() | ||||
|         .await?; | ||||
| 
 | ||||
|     Ok(res) | ||||
| } | ||||
|  | @ -0,0 +1,37 @@ | |||
| use std::collections::HashMap; | ||||
| 
 | ||||
| 
 | ||||
| use serde::Deserialize; | ||||
| 
 | ||||
| use crate::spotify_auth::Token; | ||||
| use crate::spotify_types::*; | ||||
| 
 | ||||
| 
 | ||||
| #[derive(Deserialize, Debug)] | ||||
| pub struct TopSongsResponse { | ||||
|     pub href: String, | ||||
|     pub limit: u32, // Default is 20, we don't change this.
 | ||||
|     pub next: Option<String>, | ||||
|     pub offset: u32, | ||||
|     pub previous: Option<String>, | ||||
|     pub total: u32, | ||||
|     pub items: Vec<TrackObject>, // There should be 20 of these, not guaranteed
 | ||||
| } | ||||
| 
 | ||||
| pub async fn get_top_tracks(token: &Token) -> Result<TopSongsResponse, reqwest::Error> { | ||||
|     let mut params = HashMap::new(); | ||||
|     params.insert("time_range", "short_term"); | ||||
| 
 | ||||
| 
 | ||||
|     let client = reqwest::Client::new(); | ||||
|     let res: TopSongsResponse = client.get(crate::BASE_URL.to_owned() + "/me/top/tracks") | ||||
|         .query(¶ms) | ||||
|         .header("Authorization", "Bearer ".to_owned() + &token.0) | ||||
|         .send() | ||||
|         .await? | ||||
|         .json() | ||||
|         .await?; | ||||
| 
 | ||||
|     Ok(res) | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,50 @@ | |||
| use std::collections::HashMap; | ||||
| 
 | ||||
| use sqlx::MySqlPool; | ||||
| 
 | ||||
| use crate::{spotify_types::*, spotify_auth::Token}; | ||||
| 
 | ||||
| pub async fn add_to_playlist(id: &str, uris: Vec<Uri>, token: &Token) -> Result<(), reqwest::Error> { | ||||
|     let mut params = HashMap::new(); | ||||
|     params.insert("position", "0"); // Insert at top, we could remove to append
 | ||||
| 
 | ||||
|     let mut body: HashMap<String, Vec<String>> = HashMap::new(); | ||||
|     let uris: Vec<String> = uris.iter() | ||||
|         .map(|u| &u.0) | ||||
|         .cloned() | ||||
|         .collect(); | ||||
|     body.insert("uris".to_owned(), uris); | ||||
| 
 | ||||
|     log::info!("Uri body: {:?}", body); | ||||
| 
 | ||||
|     let client = reqwest::Client::new(); | ||||
|     let res = client.post(crate::BASE_URL.to_owned() + "/playlists/" + id + "/tracks") | ||||
|         .query(¶ms) | ||||
|         .json(&body) | ||||
|         .header("Authorization", "Bearer ".to_owned() + &token.0) | ||||
|         .send() | ||||
|         .await?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| // WE CANNOT SET MORE THAN 100 AT ONCE!!!
 | ||||
| pub async fn set_playlist(id: &str, uris: Vec<Uri>, token: &Token, pool: &MySqlPool) | ||||
|     -> Result<(), reqwest::Error> { | ||||
| 
 | ||||
|     let mut body: HashMap<String, Vec<String>> = HashMap::new(); | ||||
|     let uris: Vec<String> = uris.iter() | ||||
|         .map(|u| &u.0) | ||||
|         .cloned() | ||||
|         .collect(); | ||||
|     body.insert("uris".to_owned(), uris); | ||||
| 
 | ||||
|     let client = reqwest::Client::new(); | ||||
|     let res = client.put(crate::BASE_URL.to_owned() + "/playlists/" + id + "/tracks") | ||||
|         .json(&body) | ||||
|         .header("Authorization", "Bearer ".to_owned() + &token.0) | ||||
|         .send() | ||||
|         .await?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
|  | @ -0,0 +1,37 @@ | |||
| use serde::Deserialize; | ||||
| 
 | ||||
| #[derive(Deserialize, Debug, Clone)] | ||||
| #[non_exhaustive] | ||||
| pub struct TrackObject { | ||||
|     pub album: Option<AlbumObject>, | ||||
|     pub artists: Option<Vec<ArtistObject>>, | ||||
|     pub href: String, | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
|     pub uri: Uri, | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize, Debug, Clone)] | ||||
| #[non_exhaustive] | ||||
| pub struct AlbumObject { | ||||
|     pub album_type: String, | ||||
|     pub total_tracks: u32, | ||||
|     pub href: String, | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
|     pub release_date: String, | ||||
|     pub uri: Uri, | ||||
|     pub genres: Option<String>, | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize, Debug, Clone)] | ||||
| #[non_exhaustive] | ||||
| pub struct ArtistObject { | ||||
|     pub href: String, | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize, Debug, Clone)] | ||||
| pub struct Uri(pub String); | ||||
|  | @ -0,0 +1,33 @@ | |||
| use sqlx::{MySqlPool, mysql::MySqlQueryResult}; | ||||
| use crate::spotify_types::*; | ||||
| 
 | ||||
| #[allow(non_snake_case)] // Fixes warning for macro
 | ||||
| pub async fn even_allocation(pool: &MySqlPool) -> Result<Vec<Uri>, sqlx::Error> { | ||||
|     let users: Vec<u32> = sqlx::query!(" | ||||
|         SELECT UserId From Users | ||||
|         ")
 | ||||
|         .fetch_all(pool) | ||||
|         .await? | ||||
|         .iter() | ||||
|         .map(|row| row.UserId) | ||||
|         .collect(); | ||||
| 
 | ||||
|     let mut uris: Vec<Uri> = Vec::new(); | ||||
|     let num: u32 = (100 / users.len()) as u32; | ||||
|     for user in users { | ||||
|         let mut uri_list: Vec<Uri> = sqlx::query!(" | ||||
|             SELECT Uri FROM Tracks | ||||
|             WHERE UserId = ? | ||||
|             ORDER BY RAND() | ||||
|             LIMIT ? | ||||
|             ", user, num)
 | ||||
|             .fetch_all(pool) | ||||
|             .await? | ||||
|             .iter() | ||||
|             .map(|row| Uri(row.Uri.clone())) | ||||
|             .collect(); | ||||
|         uris.append(&mut uri_list); | ||||
|     } | ||||
| 
 | ||||
|     Ok(uris) | ||||
| } | ||||
|  | @ -0,0 +1,88 @@ | |||
| use sqlx::{MySqlPool, mysql::MySqlQueryResult}; | ||||
| 
 | ||||
| use crate::spotify_auth::Token; | ||||
| 
 | ||||
| async fn add_user(email: &str, spotify_id: &str, refresh_token: &Token, pool: &MySqlPool) | ||||
|     -> Result<MySqlQueryResult, sqlx::Error> { | ||||
|     Ok(sqlx::query!( | ||||
|         "INSERT INTO spotify.Users (Email, SpotifyId, RefreshToken)
 | ||||
|         VALUES (?, ?, ?)",
 | ||||
|         email.to_owned(), | ||||
|         spotify_id.to_owned(), | ||||
|         refresh_token.0.to_owned() | ||||
|     ).execute(pool).await?) | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, PartialEq, Eq)] | ||||
| pub enum AddUserSuccess { | ||||
|     New, | ||||
|     Duplicate | ||||
| } | ||||
| 
 | ||||
| pub async fn add_user_dont_care(email: &str, spotify_id: &str, refresh_token: &Token, pool: &MySqlPool) | ||||
|     -> Result<AddUserSuccess, sqlx::Error> { | ||||
|         let q = add_user(email, spotify_id, refresh_token, pool).await; | ||||
|         match q { | ||||
|             Ok(_) => Ok(AddUserSuccess::New), | ||||
|             Err(sqlx::Error::Database(err)) => { | ||||
|                 if err.code().map(|e| e.into_owned()) == Some("23000".to_owned()) { | ||||
|                     Ok(AddUserSuccess::Duplicate) | ||||
|                 } else { | ||||
|                     Err(sqlx::Error::Database(err)) | ||||
|                 } | ||||
|             }, | ||||
|             Err(_) => Err(q.unwrap_err()) | ||||
|         } | ||||
| } | ||||
| 
 | ||||
| pub enum GetRefreshTokenByIdErr { | ||||
|     SqlxError(sqlx::Error), | ||||
|     Empty, | ||||
| } | ||||
| impl From<sqlx::Error> for GetRefreshTokenByIdErr { | ||||
|     fn from(value: sqlx::Error) -> Self { | ||||
|         GetRefreshTokenByIdErr::SqlxError(value) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[allow(non_snake_case)] // Fixes warning for macro
 | ||||
| pub async fn get_refresh_token_by_id(spotify_id: &str, pool: &MySqlPool) | ||||
|     -> Result<Token, GetRefreshTokenByIdErr> { | ||||
|     let res = sqlx::query!("SELECT RefreshToken FROM Users WHERE SpotifyId LIKE ?", | ||||
|         spotify_id.to_owned()) | ||||
|         .fetch_one(pool) | ||||
|         .await?; | ||||
|     match res.RefreshToken { | ||||
|         Some(token) => Ok(Token(token)), | ||||
|         None => Err(GetRefreshTokenByIdErr::Empty) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[allow(non_snake_case)] // Fixes warning for macro
 | ||||
| pub async fn get_refreshed_by_id(spotify_id: &str, pool: &MySqlPool) | ||||
|     -> Result<sqlx::types::time::OffsetDateTime, sqlx::Error> { | ||||
|         let res = sqlx::query!("SELECT LastRefreshed FROM Users WHERE SpotifyID LIKE ?", | ||||
|             spotify_id.to_owned()) | ||||
|             .fetch_one(pool) | ||||
|             .await?; | ||||
|         Ok(res.LastRefreshed) | ||||
| } | ||||
| 
 | ||||
| #[allow(non_snake_case)] // Fixes warning for macro
 | ||||
| pub async fn get_internal_id_by_id(spotify_id: &str, pool: &MySqlPool) | ||||
|     -> Result<u32, sqlx::Error> { | ||||
|         let res = sqlx::query!("SELECT UserId FROM Users WHERE SpotifyID LIKE ?", | ||||
|             spotify_id.to_owned()) | ||||
|             .fetch_one(pool) | ||||
|             .await?; | ||||
|         Ok(res.UserId) | ||||
| } | ||||
| 
 | ||||
| #[allow(non_snake_case)] // Fixes warning for macro
 | ||||
| pub async fn get_emilia_spotify_id(pool: &MySqlPool) | ||||
|     -> Result<String, sqlx::Error> { | ||||
|         let res = sqlx::query!("SELECT SpotifyId FROM Users WHERE IsEmilia = 1") | ||||
|             .fetch_one(pool) | ||||
|             .await?; | ||||
|         Ok(res.SpotifyId) | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| #[non_exhaustive] | ||||
| pub struct AppState { | ||||
|     pub auth_url: String, | ||||
|     pub pool: sqlx::MySqlPool, | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fn main() { | ||||
|     println!("Hello, world!"); | ||||
| } | ||||
		Loading…
	
		Reference in New Issue