From e256a9db25a6b7ceb6f334cc2e139a33df0c081d Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 18 Jan 2025 14:19:46 -0500 Subject: [PATCH] Add basic poll support --- .gitignore | 2 + src/main.rs | 237 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 228 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 78a4200..4959d59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target .idea/ votes.json +polls.json +pollvotes.json diff --git a/src/main.rs b/src/main.rs index 82f74b0..ede313d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,9 @@ use axum::{ use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; +use axum::extract::rejection::JsonRejection; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; #[derive(Debug, Clone, Serialize, Deserialize)] struct Page { @@ -13,18 +16,50 @@ struct Page { votes: i32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] struct VoteRecord { pages: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +struct PollChoice { + id: i32, + votes: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +struct Poll { + id: i32, + choices: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +struct PollRecord { + polls: Vec, +} + #[derive(Clone)] struct AppState { record: Arc>, + poll_votes: Arc> } impl AppState { fn save(&self) { + self.save_polls(); + self.save_votes(); + } + + fn save_polls(&self) { + let st = self.poll_votes.lock().unwrap().to_owned(); + serde_json::to_writer( + &std::fs::File::create("pollvotes.json").unwrap(), + &st, + ) + .expect("failed to write poll votes!"); + } + + fn save_votes(&self) { let st = self.record.lock().unwrap().to_owned(); serde_json::to_writer( &std::fs::File::create("votes.json").unwrap(), @@ -32,6 +67,77 @@ impl AppState { ) .expect("failed to write votes!"); } + + fn vote_poll(&self, poll_id: i32, choice_id: i32) { + let mut poll_votes = self.poll_votes.lock().unwrap(); + + // Ensure we even have a valid poll id + let Some(poll_config) = AppState::get_poll_config_by_id(poll_id) else { + return; + }; + + let mut poll: Option<&mut Poll> = None; + for existing_poll in &mut poll_votes.polls { + if existing_poll.id == poll_id { + poll = Some(existing_poll); + break; + } + } + if poll == None { + let mut choices = vec![]; + for choice in poll_config.choices { + choices.push(PollChoice { + id: choice.id, + votes: 0, + }); + } + + poll_votes.polls.push(Poll { + id: poll_id, + choices, + }); + poll = poll_votes.polls.last_mut(); + } + let poll = poll.unwrap(); + + for choice in &mut poll.choices { + if choice.id == choice_id { + choice.votes += 1; + break; + } + } + } + + fn get_poll_config_by_id(id: i32) -> Option { + if let Ok(data) = std::fs::read_to_string("polls.json") { + if let Ok(config) = serde_json::from_str::(&data) { + if let Some(current_poll) = config.current_poll { + let current_poll: Vec<&PollConfig> = config.polls.iter().filter(|poll| poll.id == current_poll).collect(); + if !current_poll.is_empty() { + return Some(<&PollConfig>::clone(current_poll.first().unwrap()).clone()); + } + } + } + } + + None + } + + fn get_poll_votes_by_poll_id(&self, poll_id: i32) -> Option { + let mut poll_votes = self.poll_votes.lock().unwrap(); + + let Some(poll_config) = AppState::get_poll_config_by_id(poll_id) else { + return None; + }; + + for existing_poll in &poll_votes.polls { + if existing_poll.id == poll_id { + return Some(existing_poll.clone()); + } + } + + None + } } async fn view_votes(State(state): State, Path(slug): Path) -> Json { @@ -66,28 +172,137 @@ async fn leaderboard_submit_score(State(state): State, Path(slug): Pat .push(Page { slug, votes: 1 }); } - state.save(); + state.save_votes(); +} + +#[derive(Debug, Clone, Deserialize)] +struct PollChoiceConfig { + id: i32, + name: String +} + +#[derive(Debug, Clone, Deserialize)] +struct PollConfig { + id: i32, + title: String, + choices: Vec +} + +#[derive(Debug, Clone, Deserialize)] +struct PollsConfig { + current_poll: Option, + polls: Vec, +} + +#[derive(Debug, Clone, Serialize)] +struct PollChoiceResponse { + id: i32, + name: String, + votes: i32 +} + +#[derive(Debug, Clone, Serialize)] +struct PollResponse { + title: String, + choices: Vec +} + +async fn view_current_poll(State(state): State) -> Response { + tracing::info!("Requesting current poll"); + + if let Ok(data) = std::fs::read_to_string("polls.json") { + if let Ok(config) = serde_json::from_str::(&data) { + if let Some(current_poll) = config.current_poll { + let current_poll: Vec<&PollConfig> = config.polls.iter().filter(|poll| poll.id == current_poll).collect(); + if !current_poll.is_empty() { + if let Some(poll) = state.get_poll_votes_by_poll_id(current_poll[0].id) { + let poll_config = current_poll.first().unwrap(); + let mut choices = vec![]; + + let find_vote_choice = |id: i32| { + for choice in &poll.choices { + if choice.id == id { + return Some(choice); + } + } + + return None; + }; + + for choice in &poll_config.choices { + if let Some(vote_choice) = find_vote_choice(choice.id) { + choices.push(PollChoiceResponse { + id: choice.id, + name: choice.name.clone(), + votes: vote_choice.votes, + }); + } else { + choices.push(PollChoiceResponse { + id: choice.id, + name: choice.name.clone(), + votes: 0, + }); + } + } + + return Json(PollResponse { + title: current_poll.first().unwrap().title.clone(), + choices + }).into_response(); + } + } + } + } + } + + ( + StatusCode::NOT_FOUND, + "No poll is open right now!", + ).into_response() +} + +async fn vote_current_poll(State(state): State, Path(id): Path) { + tracing::info!("Submitting vote for choice {id}"); + + if let Ok(data) = std::fs::read_to_string("polls.json") { + if let Ok(config) = serde_json::from_str::(&data) { + if let Some(current_poll) = config.current_poll { + let current_poll: Vec<&PollConfig> = config.polls.iter().filter(|poll| poll.id == current_poll).collect(); + if !current_poll.is_empty() { + state.vote_poll(current_poll.first().unwrap().id, id); + state.save_polls(); + } + } + } + } } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); - let initial_state = if let Ok(data) = std::fs::read_to_string("votes.json") { - AppState { - record: Arc::new(Mutex::new( - serde_json::from_str(&data).expect("Failed to parse"), - )), - } + let vote_record = if let Ok(data) = std::fs::read_to_string("votes.json") { + serde_json::from_str(&data).expect("Failed to parse votes data") } else { - AppState { - record: Arc::new(Mutex::new(VoteRecord { pages: vec![] })), - } + VoteRecord::default() + }; + + let poll_record = if let Ok(data) = std::fs::read_to_string("pollvotes.json") { + serde_json::from_str(&data).expect("Failed to parse polls data") + } else { + PollRecord::default() + }; + + let initial_state = AppState { + record: Arc::new(Mutex::new(vote_record)), + poll_votes: Arc::new(Mutex::new(poll_record)), }; let app = Router::new() .route("/votes/{slug}", post(leaderboard_submit_score)) .route("/votes/{slug}", get(view_votes)) + .route("/polls/current", get(view_current_poll)) + .route("/polls/vote/{id}", post(vote_current_poll)) .with_state(initial_state.clone()); let addr = SocketAddr::from(([127, 0, 0, 1], 3000));