diff --git a/src/actions.rs b/src/actions.rs index 9bb276e..b05efe8 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -60,7 +60,11 @@ impl Sentence for (String, String, String, String, HashSet, Option) } } -pub fn fill_sentences(client: &mut Client, sentences: &mut Vec, add_overrides: bool) { +pub fn fill_sentences( + client: &mut Client, + sentences: &mut Vec, + add_overrides: bool, +) { let mut queue: HashMap> = HashMap::new(); for (i, sentence) in sentences.iter().enumerate() { if queue.contains_key(&sentence.get_id()) { @@ -143,8 +147,12 @@ pub fn get_sentences( let large_enough = kanji_in_sentence.len() >= quiz_settings.min; let small_enough = kanji_in_sentence.len() <= quiz_settings.max; - if kanji_in_sentence.is_subset(&known_kanji)&& large_enough && small_enough && - (known_priority_kanji.is_empty() || !kanji_in_sentence.is_disjoint(&known_priority_kanji)) { + if kanji_in_sentence.is_subset(&known_kanji) + && large_enough + && small_enough + && (known_priority_kanji.is_empty() + || !kanji_in_sentence.is_disjoint(&known_priority_kanji)) + { sentences.push([ id.to_owned(), jap_sentence.to_owned(), @@ -162,10 +170,7 @@ pub fn get_sentences( Ok(sentences) } -pub fn generate_essay( - client: &mut Client, - quiz_settings: Form, -) -> Vec<[String; 4]> { +pub fn generate_essay(client: &mut Client, quiz_settings: Form) -> Vec<[String; 4]> { let mut essay = Vec::new(); let mut sentences = Vec::new(); let mut rng = thread_rng(); @@ -237,7 +242,12 @@ pub fn generate_essay( // Add a random sentence with a lot of known kanji to the essay let choice = tuples.choose(&mut rng).unwrap(); - essay.push([choice.0.to_owned(), choice.1.to_owned(), choice.2.to_owned(), choice.3.to_owned()]); + essay.push([ + choice.0.to_owned(), + choice.1.to_owned(), + choice.2.to_owned(), + choice.3.to_owned(), + ]); known_kanji = known_kanji.difference(&choice.4).map(|x| *x).collect(); } diff --git a/src/admin.rs b/src/admin.rs index 04b287c..4793e88 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,6 +1,9 @@ // Has functions used by routes from the admin page -use crate::{actions::{Sentence, fill_sentences}, Report, AdminReport, AdminOverride, AddOverride, EditOverride}; +use crate::{ + actions::{fill_sentences, Sentence}, + AddOverride, AdminOverride, AdminReport, EditOverride, Report, +}; use postgres::Client; use rocket::request::Form; use std::fs; @@ -114,10 +117,13 @@ pub fn get_admin_stuff(client: &mut Client) -> (i64, Vec, Vec("sentence_id").to_string()); let reported_at: chrono::DateTime = row.get("reported_at"); reports.push(AdminReport { @@ -129,7 +135,9 @@ pub fn get_admin_stuff(client: &mut Client) -> (i64, Vec, Vec (i64, Vec, Vec Result { - client.execute("UPDATE reports SET reviewed = TRUE WHERE id = $1", &[&id]).unwrap(); + client + .execute("UPDATE reports SET reviewed = TRUE WHERE id = $1", &[&id]) + .unwrap(); Ok("success".to_string()) } -pub fn add_override(client: &mut Client, override_details: Form) -> Result { +pub fn add_override( + client: &mut Client, + override_details: Form, +) -> Result { // Get the sentence ID from the report - let row = client.query_one( - "SELECT sentence_id FROM reports WHERE id = $1", - &[&override_details.report_id] - ).unwrap(); + let row = client + .query_one( + "SELECT sentence_id FROM reports WHERE id = $1", + &[&override_details.report_id], + ) + .unwrap(); let sentence_id: i32 = row.get("sentence_id"); let mut original_question = String::new(); let mut original_translation = String::new(); @@ -218,11 +235,14 @@ pub fn add_override(client: &mut Client, override_details: Form) -> let mut skip_translation = override_details.translation == original_translation; let mut skip_reading = override_details.reading == original_reading; // Compare with the existing overrides - for row in client.query( - "SELECT override_type, value FROM overrides + for row in client + .query( + "SELECT override_type, value FROM overrides WHERE sentence_id = $1 AND (primary_value = TRUE OR override_type != 'reading')", - &[&sentence_id] - ).unwrap() { + &[&sentence_id], + ) + .unwrap() + { let override_type: String = row.get("override_type"); if override_type == "question" && !skip_question { skip_question = override_details.question == row.get::<_, String>("value"); @@ -239,19 +259,51 @@ pub fn add_override(client: &mut Client, override_details: Form) -> let statement = client.prepare("INSERT INTO overrides (sentence_id, override_type, value, primary_value, report_id) VALUES ($1, 'question', $2, FALSE, $3);").or_else(|e| Err(e.to_string()))?; if !skip_question { - client.execute(&statement, &[&sentence_id, &override_details.question, &override_details.report_id]).or_else(|e| Err(e.to_string()))?; + client + .execute( + &statement, + &[ + &sentence_id, + &override_details.question, + &override_details.report_id, + ], + ) + .or_else(|e| Err(e.to_string()))?; something_changed = true; } if !skip_translation { - client.execute(&statement, &[&sentence_id, &override_details.translation, &override_details.report_id]).or_else(|e| Err(e.to_string()))?; + client + .execute( + &statement, + &[ + &sentence_id, + &override_details.translation, + &override_details.report_id, + ], + ) + .or_else(|e| Err(e.to_string()))?; something_changed = true; } if !skip_reading { - client.execute(&statement, &[&sentence_id, &override_details.reading, &override_details.report_id]).or_else(|e| Err(e.to_string()))?; + client + .execute( + &statement, + &[ + &sentence_id, + &override_details.reading, + &override_details.report_id, + ], + ) + .or_else(|e| Err(e.to_string()))?; something_changed = true; } if let Some(reading) = override_details.additional_reading.clone() { - client.execute(&statement, &[&sentence_id, &reading, &override_details.report_id]).or_else(|e| Err(e.to_string()))?; + client + .execute( + &statement, + &[&sentence_id, &reading, &override_details.report_id], + ) + .or_else(|e| Err(e.to_string()))?; something_changed = true; } if something_changed { @@ -261,16 +313,27 @@ pub fn add_override(client: &mut Client, override_details: Form) -> } } -pub fn edit_override(client: &mut Client, override_details: Form) -> Result { - client.execute( - "UPDATE overrides SET value = $1, primary_value = $2 WHERE id = $3", - &[&override_details.value, &override_details.primary_value, &override_details.override_id] - ).or_else(|e| Err(e.to_string()))?; +pub fn edit_override( + client: &mut Client, + override_details: Form, +) -> Result { + client + .execute( + "UPDATE overrides SET value = $1, primary_value = $2 WHERE id = $3", + &[ + &override_details.value, + &override_details.primary_value, + &override_details.override_id, + ], + ) + .or_else(|e| Err(e.to_string()))?; Ok(String::from("success")) } pub fn delete_override(client: &mut Client, id: i32) -> Result { client.execute("UPDATE reports SET reviewed = FALSE WHERE id = (SELECT report_id FROM overrides WHERE id = $1)", &[&id]).or_else(|e| Err(e.to_string()))?; - client.execute("DELETE FROM overrides WHERE id = $1", &[&id]).or_else(|e| Err(e.to_string()))?; + client + .execute("DELETE FROM overrides WHERE id = $1", &[&id]) + .or_else(|e| Err(e.to_string()))?; Ok(String::from("success")) } diff --git a/src/kanji_import.rs b/src/kanji_import.rs index c1ada47..c4424aa 100644 --- a/src/kanji_import.rs +++ b/src/kanji_import.rs @@ -4,8 +4,13 @@ use super::OrderedImport; use regex::Regex; use rocket::{http::Status, request::Form, response::status::Custom}; use rusqlite::{Connection, NO_PARAMS}; +use std::{ + collections::HashSet, + error::Error, + fs, + io::{Cursor, Read, Write}, +}; use uuid::Uuid; -use std::{error::Error, collections::HashSet, io::{Cursor, Read, Write}, fs}; pub enum KanjiOrder { WaniKani, @@ -29,12 +34,10 @@ pub fn extract_kanji_from_anki_deck( println!("An error occurred: {:?}", e); // Delete the database file since we're returning early from an error fs::remove_file(file_name).expect("Couldn't delete the anki database file"); - let _ = fs::remove_file(&format!("{}-shm", file_name)); // Delete if exists - let _ = fs::remove_file(&format!("{}-wal", file_name)); // Delete if exists - Err(Custom( - Status::InternalServerError, - e.to_string(), - )) + // Delete the temp db files if they exist + let _ = fs::remove_file(&format!("{}-shm", file_name)); + let _ = fs::remove_file(&format!("{}-wal", file_name)); + Err(Custom(Status::InternalServerError, e.to_string())) } let mut contents = Vec::new(); @@ -42,49 +45,57 @@ pub fn extract_kanji_from_anki_deck( if let Ok(file) = zip.by_name("collection.anki21b") { // This deck uses the Anki scheduler v3 // This requires us to use zstd to decompress the file - zstd::stream::copy_decode(file, &mut contents).or_else(|e| return_err(&file_name, e))?; + zstd::stream::copy_decode(file, &mut contents) + .or_else(|e| return_err(&file_name, e))?; } if contents.len() == 0 { if let Ok(mut file) = zip.by_name("collection.anki21") { // This deck uses the Anki 2.1 scheduler - file.read_to_end(&mut contents).or_else(|e| return_err(&file_name, e))?; + file.read_to_end(&mut contents) + .or_else(|e| return_err(&file_name, e))?; } } if contents.len() == 0 { if let Ok(mut file) = zip.by_name("collection.anki2") { // This deck doesn't use the Anki 2.1 scheduler - file.read_to_end(&mut contents).or_else(|e| return_err(&file_name, e))?; + file.read_to_end(&mut contents) + .or_else(|e| return_err(&file_name, e))?; } } if contents.len() > 0 { // We now have the sqlite3 database with the notes // Write the database to a file let mut f = fs::File::create(&file_name).or_else(|e| return_err(&file_name, e))?; - f.write_all(&contents).or_else(|e| return_err(&file_name, e))?; + f.write_all(&contents) + .or_else(|e| return_err(&file_name, e))?; let conn = Connection::open(&file_name).or_else(|e| return_err(&file_name, e))?; // Create a variable to store the kanji let mut kanji: HashSet = HashSet::new(); // Regex to find kanji let kanji_regex = Regex::new(r"[\p{Han}]").unwrap(); /* - * In most decks I checked the kanji was in the sort field (sfld) column, but some - * decks have numbers there, and the kanji is in the fields (flds) column. In this - * case it's more complicated because there can be multiple fields and the kanji - * could be in any one of those fields. So we take the sfld column if it has kanji, - * otherwise as a secondary option we take the flds column. - * - * The queue column in the cards table tells us if the card is already learnt, is - * being learnt, or has never been seen before. if the only_learnt parameter is - * true, we should only consider cards that are in queue 2 (learnt). - * - * Despite the DISTINCT clause, it is still necessary to filter duplicates because - * different notes of the same kanji could be in different queues. - */ - let mut statement = conn.prepare( - "SELECT DISTINCT cards.queue, notes.sfld, notes.flds - FROM cards INNER JOIN notes on notes.id = cards.nid" - ).or_else(|e| return_err(&file_name, e))?; - let mut rows = statement.query(NO_PARAMS).or_else(|e| return_err(&file_name, e))?; + * In most decks I checked the kanji was in the sort field (sfld) column, but some + * decks have numbers there, and the kanji is in the fields (flds) column. In this + * case it's more complicated because there can be multiple fields and the kanji + * could be in any one of those fields. So we take the sfld column if it has kanji, + * otherwise as a secondary option we take the flds column. + * + * The queue column in the cards table tells us if the card is already learnt, is + * being learnt, or has never been seen before. if the only_learnt parameter is + * true, we should only consider cards that are in queue 2 (learnt). + * + * Despite the DISTINCT clause, it is still necessary to filter duplicates because + * different notes of the same kanji could be in different queues. + */ + let mut statement = conn + .prepare( + "SELECT DISTINCT cards.queue, notes.sfld, notes.flds + FROM cards INNER JOIN notes on notes.id = cards.nid", + ) + .or_else(|e| return_err(&file_name, e))?; + let mut rows = statement + .query(NO_PARAMS) + .or_else(|e| return_err(&file_name, e))?; while let Some(row) = rows.next().unwrap() { if !only_learnt || row.get::<_, i32>(0).unwrap() == 2 { let mut no_kanji_found = true; @@ -108,8 +119,9 @@ pub fn extract_kanji_from_anki_deck( } // Delete the database file fs::remove_file(&file_name).expect("Couldn't delete the anki database file"); - let _ = fs::remove_file(&format!("{}-shm", file_name)); // Delete if exists - let _ = fs::remove_file(&format!("{}-wal", file_name)); // Delete if exists + // Delete the temp db files if they exist + let _ = fs::remove_file(&format!("{}-shm", file_name)); + let _ = fs::remove_file(&format!("{}-wal", file_name)); // Return all the extracted kanji return Ok(kanji .iter() @@ -179,7 +191,12 @@ pub fn kanji_from_wanikani(api_key: &str) -> Result> { // Since we restrict to 1000 ids at a time, we shouldn't need to paginate url = String::from("https://api.wanikani.com/v2/subjects"); let start = i * 1000; - let end = start + if i == ids.len() / 1000 {ids.len() % 1000} else {1000}; + let end = start + + if i == ids.len() / 1000 { + ids.len() % 1000 + } else { + 1000 + }; let json = client .get(&url) diff --git a/src/main.rs b/src/main.rs index 6500293..db43551 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,20 +5,23 @@ extern crate rocket; #[macro_use] extern crate serde_derive; +use argon2::{ + password_hash::{PasswordHash, PasswordVerifier}, + Argon2, +}; use dotenv::dotenv; use io::Read; use multipart::server::Multipart; use native_tls::TlsConnector; -use postgres_native_tls::MakeTlsConnector; use postgres::Client; +use postgres_native_tls::MakeTlsConnector; use rocket::{ - http::{ContentType, Cookies, Cookie, Status}, + http::{ContentType, Cookie, Cookies, Status}, request::Form, response::status::Custom, Config, Data, State, }; -use rocket_contrib::{serve::StaticFiles, templates::Template, json::Json}; -use argon2::{password_hash::{PasswordHash, PasswordVerifier}, Argon2}; +use rocket_contrib::{json::Json, serve::StaticFiles, templates::Template}; use std::{ collections::HashMap, env, @@ -27,12 +30,12 @@ use std::{ }; mod actions; -mod kanji_import; mod admin; +mod kanji_import; use actions::*; -use kanji_import::*; use admin::*; +use kanji_import::*; #[derive(FromForm)] pub struct QuizSettings { @@ -271,7 +274,10 @@ fn post_import_kanken(import_settings: Form) -> Result>, quiz_settings: Form) -> Json> { +fn post_essay( + client: State>, + quiz_settings: Form, +) -> Json> { Json(generate_essay(&mut client.lock().unwrap(), quiz_settings)) } @@ -280,7 +286,10 @@ fn post_admin_signin(password: Form, mut cookies: Cookies) -> Strin let argon2 = Argon2::default(); let admin_hash = env::var("ADMIN_HASH").unwrap(); let parsed_hash = PasswordHash::new(&admin_hash).unwrap(); - if argon2.verify_password(password.value.as_bytes(), &parsed_hash).is_ok() { + if argon2 + .verify_password(password.value.as_bytes(), &parsed_hash) + .is_ok() + { cookies.add_private(Cookie::new("admin_hash", admin_hash)); String::from("success") } else { @@ -289,17 +298,28 @@ fn post_admin_signin(password: Form, mut cookies: Cookies) -> Strin } #[post("/delete_report", data = "")] -fn post_delete_report(client: State>, report_id: Form, mut cookies: Cookies) -> Result { +fn post_delete_report( + client: State>, + report_id: Form, + mut cookies: Cookies, +) -> Result { if let Some(hash) = cookies.get_private("admin_hash") { if hash.value() == env::var("ADMIN_HASH").expect("Env var ADMIN_HASH not found") { - return mark_reviewed(&mut client.lock().unwrap(), report_id.value.parse().unwrap()); + return mark_reviewed( + &mut client.lock().unwrap(), + report_id.value.parse().unwrap(), + ); } } Err("Error: not signed in".to_string()) } #[post("/add_override", data = "")] -fn post_add_override(client: State>, override_details: Form, mut cookies: Cookies) -> Result { +fn post_add_override( + client: State>, + override_details: Form, + mut cookies: Cookies, +) -> Result { if let Some(hash) = cookies.get_private("admin_hash") { if hash.value() == env::var("ADMIN_HASH").expect("Env var ADMIN_HASH not found") { return add_override(&mut client.lock().unwrap(), override_details); @@ -309,20 +329,31 @@ fn post_add_override(client: State>, override_details: Form>, override_id: Form, mut cookies: Cookies) -> Result { +fn post_delete_override( + client: State>, + override_id: Form, + mut cookies: Cookies, +) -> Result { if let Some(hash) = cookies.get_private("admin_hash") { if hash.value() == env::var("ADMIN_HASH").expect("Env var ADMIN_HASH not found") { - return delete_override(&mut client.lock().unwrap(), override_id.value.parse().unwrap()); + return delete_override( + &mut client.lock().unwrap(), + override_id.value.parse().unwrap(), + ); } } Err("Error: not signed in".to_string()) } #[post("/edit_override", data = "")] -fn post_edit_override(client: State>, override_details: Form, mut cookies: Cookies) -> Result { +fn post_edit_override( + client: State>, + override_details: Form, + mut cookies: Cookies, +) -> Result { if let Some(hash) = cookies.get_private("admin_hash") { if hash.value() == env::var("ADMIN_HASH").expect("Env var ADMIN_HASH not found") { - return edit_override(&mut client.lock().unwrap(), override_details) + return edit_override(&mut client.lock().unwrap(), override_details); } } Err("Error: not signed in".to_string()) @@ -389,8 +420,17 @@ fn rocket() -> rocket::Rocket { fn main() { dotenv().ok(); - let connector = MakeTlsConnector::new(TlsConnector::builder().danger_accept_invalid_certs(true).build().unwrap()); + let connector = MakeTlsConnector::new( + TlsConnector::builder() + .danger_accept_invalid_certs(true) + .build() + .unwrap(), + ); - let client = Client::connect(&env::var("DATABASE_URL").expect("Env var DATABASE_URL not found"), connector).unwrap(); + let client = Client::connect( + &env::var("DATABASE_URL").expect("Env var DATABASE_URL not found"), + connector, + ) + .unwrap(); rocket().manage(Mutex::new(client)).launch(); }