diff --git a/Rocket.toml b/Rocket.toml new file mode 100644 index 0000000..378e4fe --- /dev/null +++ b/Rocket.toml @@ -0,0 +1,2 @@ +[default] +address = "0.0.0.0" diff --git a/src/main.rs b/src/main.rs index cc3570d..2dc7b8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,37 @@ use std::env; use rocket::{ - get, post, routes, - serde::{json::Json, Deserialize, Serialize}, - State, patch, http::Status, fairing::{Fairing, Info}, + get, + post, + routes, + serde::{ json::Json, Deserialize, Serialize }, + State, + patch, + http::Status, + fairing::{ Fairing, Info }, }; -use sqlx::{mysql::MySqlPoolOptions, FromRow, MySql, Pool, Row}; +use sqlx::{ mysql::MySqlPoolOptions, FromRow, MySql, Pool, Row }; + +use std::fmt; + +#[derive(Debug)] +struct ValidationError { + message: String, +} + +impl fmt::Display for ValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for ValidationError {} + +impl From for sqlx::Error { + fn from(err: ValidationError) -> Self { + sqlx::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())) + } +} #[derive(Deserialize, Serialize, FromRow)] #[serde(crate = "rocket::serde")] @@ -34,7 +60,7 @@ struct QueueSong { #[serde(crate = "rocket::serde")] struct QueuePostPayload { queueItem: QueueSong, - uuid: String + uuid: String, } #[derive(Deserialize)] @@ -47,19 +73,18 @@ struct SongPayload { song_id: Option, } - #[derive(Deserialize)] #[serde(crate = "rocket::serde")] struct QueueUpdatePayload { queueid: i32, - uuid: String + uuid: String, } #[derive(Deserialize)] #[serde(crate = "rocket::serde")] struct QueueClearPayload { uuid: String, - key: String + key: String, } #[derive(FromRow)] @@ -68,7 +93,7 @@ struct Usage { tst: String, twitch_id: i32, twitch_name: String, - vs: String, + vs: Option, playertype: Option, access_key: Option, } @@ -101,19 +126,33 @@ struct History { tst: String, } +#[derive(Deserialize, Serialize, FromRow)] +#[serde(crate = "rocket::serde")] +struct MotdMessage { + id: i32, + message_text: String, + severity: String, + created_at: String, + start_date: Option, + end_date: Option, + is_active: bool, + author: Option, +} + + struct Cors; fn get_custom_uuids(uuid: &str) -> &str { match uuid { - "inzaniity" => "43efb299-2504-4365-8ac6-a301f0d7c7aa", - "thejaydizzle" => "5d07c1d6-6dcc-4185-a6bd-284fe0480b79", - "sluckz" => "f6d9a390-7d48-4da6-a177-c378a7a33c1e", - "vigilsc" => "2580091a-aec1-44be-afbd-274523c1b3d2", - "itsbustre" => "c90b6e0e-6706-4036-bf25-327b2d981082", - "rocketstarrl" => "de8a9f85-2919-474c-9845-6534ec54dc7f", - "preheet" => "4aa39d0a-1bf6-4705-bfb5-512dd8afc1e2", - "highitsky" => "630e6596-a833-42d9-a905-7a5bf1a75d0e", - _ => uuid + "inzaniity" => "43efb299-2504-4365-8ac6-a301f0d7c7aa", + "thejaydizzle" => "5d07c1d6-6dcc-4185-a6bd-284fe0480b79", + "sluckz" => "f6d9a390-7d48-4da6-a177-c378a7a33c1e", + "vigilsc" => "07632164-719f-43ee-87eb-a1c9b4991506", + "itsbustre" => "c90b6e0e-6706-4036-bf25-327b2d981082", + "rocketstarrl" => "de8a9f85-2919-474c-9845-6534ec54dc7f", + "preheet" => "4aa39d0a-1bf6-4705-bfb5-512dd8afc1e2", + "highitsky" => "630e6596-a833-42d9-a905-7a5bf1a75d0e", + _ => uuid, } } @@ -126,45 +165,75 @@ impl Fairing for Cors { } } - async fn on_response<'r>(&self, _request: &'r rocket::Request<'_>, response: &mut rocket::Response<'r>) { + async fn on_response<'r>( + &self, + _request: &'r rocket::Request<'_>, + response: &mut rocket::Response<'r> + ) { response.set_header(rocket::http::Header::new("Access-Control-Allow-Origin", "*")); - response.set_header(rocket::http::Header::new("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PATCH, DELETE")); - response.set_header(rocket::http::Header::new("Access-Control-Allow-Headers", "Content-Type")); + response.set_header( + rocket::http::Header::new( + "Access-Control-Allow-Methods", + "POST, GET, OPTIONS, PATCH, DELETE" + ) + ); + response.set_header( + rocket::http::Header::new("Access-Control-Allow-Headers", "Content-Type") + ); } } impl History { pub async fn set_history(history: Self, pool: &Pool) -> sqlx::Result<()> { - sqlx::query("INSERT INTO songify_history (uuid, song, tst) VALUES (?, ?, ?)") + sqlx + ::query("INSERT INTO songify_history (uuid, song, tst) VALUES (?, ?, ?)") .bind(history.uuid) .bind(history.song) .bind(history.tst) - .execute(pool) - .await?; + .execute(pool).await?; Ok(()) } pub async fn get_history(id: String, pool: &Pool) -> Result, sqlx::Error> { let uuid = get_custom_uuids(&id); - let history = sqlx::query_as::("SELECT * FROM songify_history WHERE uuid = ? ORDER BY tst DESC") + let history = sqlx + ::query_as::( + "SELECT * FROM songify_history WHERE uuid = ? ORDER BY tst DESC" + ) .bind(uuid) - .fetch_all(pool) - .await?; + .fetch_all(pool).await?; Ok(history) } } +impl MotdMessage { + pub async fn get_active_motd(pool: &Pool) -> Result, sqlx::Error> { + let motd = sqlx::query_as::( + "SELECT id, message_text, severity, created_at, start_date, end_date, is_active, author + FROM MotdMessages + WHERE is_active = TRUE + AND (start_date IS NULL OR start_date <= NOW()) + AND (end_date IS NULL OR end_date >= NOW()) + ORDER BY created_at DESC" + ) + .fetch_all(pool) + .await?; + + Ok(motd) + } +} + impl Song { pub async fn set_song(song: Self, pool: &Pool) -> sqlx::Result<()> { - let result = sqlx::query("REPLACE INTO song_data (UUID, song, cover_url, song_id) VALUES (?, ?, ?, ?)") + let result = sqlx + ::query("REPLACE INTO song_data (UUID, song, cover_url, song_id) VALUES (?, ?, ?, ?)") .bind(song.uuid) .bind(song.song) .bind(song.cover_url) .bind(song.song_id) - .execute(pool) - .await; + .execute(pool).await; if let Err(e) = result { println!("Error: {}", e); @@ -174,28 +243,30 @@ impl Song { } pub async fn get_song(id: String, pool: &Pool) -> Result { - let song = sqlx::query_as::("SELECT * FROM song_data WHERE uuid = ?") + let song = sqlx + ::query_as::("SELECT * FROM song_data WHERE uuid = ?") .bind(&id) - .fetch_one(pool) - .await; - - song.map_or_else(|_| Ok(Self { - uuid: id, - song: "No song found".to_string(), - cover_url: String::new(), - song_id: None - }), Ok) + .fetch_one(pool).await; + + song.map_or_else( + |_| + Ok(Self { + uuid: id, + song: "No song found".to_string(), + cover_url: String::new(), + song_id: None, + }), + Ok + ) } } impl QueueSong { pub async fn get_queue(id: String, pool: &Pool) -> Result, sqlx::Error> { - let queue = sqlx::query_as::( - "SELECT * FROM songify_queue WHERE Uuid = ? AND Played = 0;", - ) - .bind(id) - .fetch_all(pool) - .await?; + let queue = sqlx + ::query_as::("SELECT * FROM songify_queue WHERE Uuid = ? AND Played = 0;") + .bind(id) + .fetch_all(pool).await?; Ok(queue) } @@ -203,8 +274,10 @@ impl QueueSong { pub async fn add_to_queue(id: String, song: Self, pool: &Pool) -> sqlx::Result { use sqlx::Row; - let inserted_song = sqlx::query( - "INSERT INTO songify_queue (Queueid, Uuid, Trackid, Artist, Title, Length, Requester, Played, Albumcover) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *") + let inserted_song = sqlx + ::query( + "INSERT INTO songify_queue (Queueid, Uuid, Trackid, Artist, Title, Length, Requester, Played, Albumcover) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *" + ) .bind(id) .bind(&song.Trackid) .bind(&song.Artist) @@ -213,8 +286,7 @@ impl QueueSong { .bind(&song.Requester) .bind(0) .bind(&song.Albumcover) - .fetch_one(pool) - .await?; + .fetch_one(pool).await?; Ok(Self { Queueid: inserted_song.get(0), @@ -229,21 +301,25 @@ impl QueueSong { }) } - pub async fn remove_from_queue(uuid: String, queueid: i32, pool: &Pool) -> sqlx::Result<()> { - sqlx::query("UPDATE songify_queue SET Played = 1 WHERE Uuid = ? AND Queueid = ?") + pub async fn remove_from_queue( + uuid: String, + queueid: i32, + pool: &Pool + ) -> sqlx::Result<()> { + sqlx + ::query("UPDATE songify_queue SET Played = 1 WHERE Uuid = ? AND Queueid = ?") .bind(uuid) .bind(queueid) - .execute(pool) - .await?; + .execute(pool).await?; Ok(()) } pub async fn clear_queue(uuid: String, pool: &Pool) -> sqlx::Result<()> { - sqlx::query("UPDATE songify_queue SET Played = 1 WHERE Uuid = ?") + sqlx + ::query("UPDATE songify_queue SET Played = 1 WHERE Uuid = ?") .bind(uuid) - .execute(pool) - .await?; + .execute(pool).await?; Ok(()) } @@ -252,39 +328,46 @@ impl QueueSong { impl Usage { pub async fn get_access_key( id: String, - pool: &Pool, + pool: &Pool ) -> Result, sqlx::Error> { - let usage = - sqlx::query("SELECT access_key FROM songify_usage WHERE UUID = ?") - .bind(id) - .fetch_one(pool) - .await; + let usage = sqlx + ::query("SELECT access_key FROM songify_usage WHERE UUID = ?") + .bind(id) + .fetch_one(pool).await; match usage { Ok(usage) => Ok(Some(usage.get(0))), Err(sqlx::Error::RowNotFound) => Ok(None), - Err(e) => { - Err(e) - }, + Err(e) => { Err(e) } } } pub async fn set_access_key( id: String, access_key: String, - pool: &Pool, + pool: &Pool ) -> sqlx::Result<()> { - sqlx::query("UPDATE songify_usage SET access_key = ? WHERE UUID = ?") + sqlx + ::query("UPDATE songify_usage SET access_key = ? WHERE UUID = ?") .bind(access_key) .bind(id) - .execute(pool) - .await?; + .execute(pool).await?; Ok(()) } - pub async fn set_telemetry(telemetry: Telemetry, pool: &Pool,) -> sqlx::Result<()> { - sqlx::query("REPLACE INTO songify_usage (UUID, tst, twitch_id, twitch_name, vs, playertype, access_key) VALUES (?, ?, ?, ?, ?, ?, ?)") + pub async fn set_telemetry(telemetry: Telemetry, pool: &Pool) -> sqlx::Result<()> { + if telemetry.uuid.is_empty() { + return Err( + (ValidationError { + message: "UUID cannot be empty".to_string(), + }).into() + ); + } + sqlx + ::query( + "REPLACE INTO songify_usage (UUID, tst, twitch_id, twitch_name, vs, playertype, access_key) VALUES (?, ?, ?, ?, ?, ?, ?)" + ) .bind(telemetry.uuid) .bind(telemetry.tst.to_string()) .bind(telemetry.twitch_id) @@ -292,40 +375,51 @@ impl Usage { .bind(telemetry.vs) .bind(telemetry.playertype) .bind(telemetry.key) - .execute(pool) - .await?; + .execute(pool).await?; Ok(()) } - pub async fn get_twitch_name(id: String, pool: &Pool) -> Result, sqlx::Error> { - let usage = - sqlx::query("SELECT twitch_name FROM songify_usage WHERE UUID = ?") - .bind(id) - .fetch_one(pool) - .await; + pub async fn get_twitch_name( + id: String, + pool: &Pool + ) -> Result, sqlx::Error> { + let usage = sqlx + ::query("SELECT twitch_name FROM songify_usage WHERE UUID = ?") + .bind(id) + .fetch_one(pool).await; match usage { Ok(usage) => Ok(Some(usage.get(0))), Err(sqlx::Error::RowNotFound) => Ok(None), - Err(e) => { - Err(e) - }, + Err(e) => { Err(e) } } } } -async fn verify_access_key(uuid: &str, api_key: &str, pool: &State>) -> Result<(), Status> { - let access_key = Usage::get_access_key(uuid.to_string(), pool).await.map_err(|_| Status::InternalServerError)?; + + +async fn verify_access_key( + uuid: &str, + api_key: &str, + pool: &State> +) -> Result<(), Status> { + let access_key = Usage::get_access_key(uuid.to_string(), pool).await.map_err( + |_| Status::InternalServerError + )?; match access_key { Some(key) => { if key != api_key { + println!("Access key mismatch"); + println!("{} != {}", key, api_key); return Err(Status::Unauthorized); } } None => { - Usage::set_access_key(uuid.to_string(), api_key.to_string(), pool).await.map_err(|_| Status::InternalServerError)?; + Usage::set_access_key(uuid.to_string(), api_key.to_string(), pool).await.map_err( + |_| Status::InternalServerError + )?; } } @@ -334,34 +428,47 @@ async fn verify_access_key(uuid: &str, api_key: &str, pool: &State>) #[get("/getsong?")] async fn get_song(pool: &State>, uuid: &str) -> Result { - Song::get_song(uuid.to_string(), pool).await.map_or(Err(Status::InternalServerError), |song| Ok(song.song)) + Song::get_song(uuid.to_string(), pool).await.map_or(Err(Status::InternalServerError), |song| + Ok(song.song) + ) } #[get("/getcover?")] async fn get_cover(pool: &State>, uuid: &str) -> Result { - Song::get_song(uuid.to_string(), pool).await.map_or(Err(Status::InternalServerError), |song| Ok(song.cover_url)) + Song::get_song(uuid.to_string(), pool).await.map_or(Err(Status::InternalServerError), |song| + Ok(song.cover_url) + ) } #[get("/queue?")] async fn get_queue(pool: &State>, uuid: &str) -> Result>, Status> { - QueueSong::get_queue(uuid.to_string(), pool).await.map_or(Err(Status::InternalServerError), |queue| Ok(Json(queue))) + QueueSong::get_queue(uuid.to_string(), pool).await.map_or( + Err(Status::InternalServerError), + |queue| Ok(Json(queue)) + ) } #[post("/queue?", format = "json", data = "")] async fn add_to_queue( pool: &State>, api_key: &str, - song: Json, + song: Json ) -> Result, Status> { let song = song.into_inner(); verify_access_key(&song.uuid, api_key, pool).await?; - (QueueSong::add_to_queue(song.uuid, song.queueItem, pool) - .await).map_or(Err(Status::InternalServerError), |song| Ok(Json(song))) + QueueSong::add_to_queue(song.uuid, song.queueItem, pool).await.map_or( + Err(Status::InternalServerError), + |song| Ok(Json(song)) + ) } #[patch("/queue?", format = "json", data = "")] -async fn set_queue_song_played(pool: &State>, api_key: &str, song: Json) -> Result<(), Status> { +async fn set_queue_song_played( + pool: &State>, + api_key: &str, + song: Json +) -> Result<(), Status> { let song = song.into_inner(); verify_access_key(&song.uuid, api_key, pool).await?; @@ -376,7 +483,11 @@ async fn set_queue_song_played(pool: &State>, api_key: &str, song: J } #[post("/queue_delete?", format = "json", data = "")] -async fn clear_queue(pool: &State>, api_key: &str, queue: Json) -> Result<(), Status> { +async fn clear_queue( + pool: &State>, + api_key: &str, + queue: Json +) -> Result<(), Status> { let queue = queue.into_inner(); verify_access_key(&queue.uuid, api_key, pool).await?; @@ -391,14 +502,21 @@ async fn clear_queue(pool: &State>, api_key: &str, queue: Json>, telemetry: Json) -> Result<(), Status> { +async fn set_telemetry( + pool: &State>, + telemetry: Json +) -> Result<(), Status> { let data = telemetry.into_inner(); verify_access_key(&data.uuid, &data.key, pool).await?; Usage::set_telemetry(data, pool).await.map_or(Err(Status::InternalServerError), |_| Ok(())) } #[post("/song?", format = "json", data = "")] -async fn set_song(pool: &State>, api_key: String, song: Json) -> Result<(), Status> { +async fn set_song( + pool: &State>, + api_key: String, + song: Json +) -> Result<(), Status> { let data = song.into_inner(); let cover = data.cover.map_or_else(String::new, |cover| cover); @@ -415,7 +533,11 @@ async fn set_song(pool: &State>, api_key: String, song: Json", format = "json", data = "")] -async fn set_history(pool: &State>, api_key: String, payload: Json) -> Result<(), Status> { +async fn set_history( + pool: &State>, + api_key: String, + payload: Json +) -> Result<(), Status> { let payload = payload.into_inner(); verify_access_key(&payload.id, &api_key, pool).await?; @@ -428,58 +550,80 @@ async fn set_history(pool: &State>, api_key: String, payload: Json>) -> Result>, Status> { + MotdMessage::get_active_motd(pool).await.map_or( + Err(Status::InternalServerError), + |motd| Ok(Json(motd)) + ) +} + + #[get("/history_data?")] -async fn get_history_data(pool: &State>, id: String) -> Result>, Status> { - History::get_history(id, pool).await.map_or(Err(Status::InternalServerError), |history| Ok(Json(history))) +async fn get_history_data( + pool: &State>, + id: String +) -> Result>, Status> { + History::get_history(id, pool).await.map_or(Err(Status::InternalServerError), |history| + Ok(Json(history)) + ) } #[get("/twitch_name?")] async fn get_twitch_name(pool: &State>, id: String) -> Result { - Usage::get_twitch_name(id, pool).await.map_or(Err(Status::InternalServerError), |name| Ok(match name { - Some(name) => name, - None => String::new(), - })) + Usage::get_twitch_name(id, pool).await.map_or(Err(Status::InternalServerError), |name| + Ok(match name { + Some(name) => name, + None => String::new(), + }) + ) } - #[rocket::main] async fn main() -> Result<(), rocket::Error> { - let database_url = env::var("DATABASE_URL") - .map_or_else(|_| { - println!("No database url found"); - std::process::exit(1); - }, |url| url); - + let database_url = env::var("DATABASE_URL").map_or_else( + |_| { + println!("No database url found"); + std::process::exit(1); + }, + |url| url + ); let pool = MySqlPoolOptions::new() .max_connections(5) - .connect(&database_url) - .await - .map_or_else(|_| { - println!("Could not connect to database"); - std::process::exit(1); - }, |pool| pool); - - - - rocket::build() - .mount("/v2", routes![ - get_queue, - add_to_queue, - set_queue_song_played, - clear_queue, - set_telemetry, - get_song, - set_song, - get_cover, - set_history, - get_history_data, - get_twitch_name - ]) + .connect(&database_url).await + .map_or_else( + |_| { + println!("Could not connect to database"); + std::process::exit(1); + }, + |pool| pool + ); + + println!("running v2 :)"); + + rocket + ::build() + .mount( + "/v2", + routes![ + get_queue, + add_to_queue, + set_queue_song_played, + clear_queue, + set_telemetry, + get_song, + set_song, + get_cover, + set_history, + get_history_data, + get_twitch_name, + get_motd + ] + ) .manage(pool) .attach(Cors) - .launch() - .await?; + .launch().await?; Ok(()) } diff --git a/src/main.rs_236_13 b/src/main.rs_236_13 new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs_288_24 b/src/main.rs_288_24 new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs_323_30 b/src/main.rs_323_30 new file mode 100644 index 0000000..e69de29