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
|
/target
|
||||||
.idea/
|
.idea/
|
||||||
votes.json
|
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 serde::{Deserialize, Serialize};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use axum::extract::rejection::JsonRejection;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct Page {
|
struct Page {
|
||||||
|
@ -13,18 +16,50 @@ struct Page {
|
||||||
votes: i32,
|
votes: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
struct VoteRecord {
|
struct VoteRecord {
|
||||||
pages: Vec<Page>,
|
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)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
record: Arc<Mutex<VoteRecord>>,
|
record: Arc<Mutex<VoteRecord>>,
|
||||||
|
poll_votes: Arc<Mutex<PollRecord>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
fn save(&self) {
|
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();
|
let st = self.record.lock().unwrap().to_owned();
|
||||||
serde_json::to_writer(
|
serde_json::to_writer(
|
||||||
&std::fs::File::create("votes.json").unwrap(),
|
&std::fs::File::create("votes.json").unwrap(),
|
||||||
|
@ -32,6 +67,77 @@ impl AppState {
|
||||||
)
|
)
|
||||||
.expect("failed to write votes!");
|
.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> {
|
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 });
|
.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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let initial_state = if let Ok(data) = std::fs::read_to_string("votes.json") {
|
let vote_record = if let Ok(data) = std::fs::read_to_string("votes.json") {
|
||||||
AppState {
|
serde_json::from_str(&data).expect("Failed to parse votes data")
|
||||||
record: Arc::new(Mutex::new(
|
|
||||||
serde_json::from_str(&data).expect("Failed to parse"),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
AppState {
|
VoteRecord::default()
|
||||||
record: Arc::new(Mutex::new(VoteRecord { pages: vec![] })),
|
};
|
||||||
}
|
|
||||||
|
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()
|
let app = Router::new()
|
||||||
.route("/votes/{slug}", post(leaderboard_submit_score))
|
.route("/votes/{slug}", post(leaderboard_submit_score))
|
||||||
.route("/votes/{slug}", get(view_votes))
|
.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());
|
.with_state(initial_state.clone());
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||||
|
|
Loading…
Add table
Reference in a new issue