Add basic poll support
This commit is contained in:
parent
71e6e2cbef
commit
e256a9db25
2 changed files with 228 additions and 11 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
/target
|
||||
.idea/
|
||||
votes.json
|
||||
polls.json
|
||||
pollvotes.json
|
||||
|
|
237
src/main.rs
237
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<Page>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
struct PollChoice {
|
||||
id: i32,
|
||||
votes: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
struct Poll {
|
||||
id: i32,
|
||||
choices: Vec<PollChoice>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
struct PollRecord {
|
||||
polls: Vec<Poll>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
record: Arc<Mutex<VoteRecord>>,
|
||||
poll_votes: Arc<Mutex<PollRecord>>
|
||||
}
|
||||
|
||||
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<PollConfig> {
|
||||
if let Ok(data) = std::fs::read_to_string("polls.json") {
|
||||
if let Ok(config) = serde_json::from_str::<PollsConfig>(&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<Poll> {
|
||||
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<AppState>, Path(slug): Path<String>) -> Json<Page> {
|
||||
|
@ -66,28 +172,137 @@ async fn leaderboard_submit_score(State(state): State<AppState>, 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<PollChoiceConfig>
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct PollsConfig {
|
||||
current_poll: Option<i32>,
|
||||
polls: Vec<PollConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct PollChoiceResponse {
|
||||
id: i32,
|
||||
name: String,
|
||||
votes: i32
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct PollResponse {
|
||||
title: String,
|
||||
choices: Vec<PollChoiceResponse>
|
||||
}
|
||||
|
||||
async fn view_current_poll(State(state): State<AppState>) -> 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::<PollsConfig>(&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<AppState>, Path(id): Path<i32>) {
|
||||
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::<PollsConfig>(&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));
|
||||
|
|
Loading…
Add table
Reference in a new issue