diff --git a/Cargo.lock b/Cargo.lock index cd780ac..9d0873d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,26 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -20,6 +40,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c70beb79cbb5ce9c4f8e20849978f34225931f665bb49efa6982875a4d5facb3" + [[package]] name = "block-buffer" version = "0.10.4" @@ -59,6 +85,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time", + "wasm-bindgen", + "winapi", +] + [[package]] name = "clipboard-win" version = "4.5.0" @@ -70,6 +112,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.6" @@ -89,6 +153,63 @@ dependencies = [ "typenum", ] +[[package]] +name = "cxx" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.13", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.6" @@ -163,6 +284,18 @@ dependencies = [ "str-buf", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fd-lock" version = "3.0.12" @@ -265,10 +398,28 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown", +] + [[package]] name = "hermit-abi" version = "0.2.6" @@ -301,6 +452,30 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "idna" version = "0.3.0" @@ -371,6 +546,26 @@ version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.3.1" @@ -410,7 +605,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.45.0", ] @@ -439,11 +634,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg", - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -521,6 +735,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -591,7 +811,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -614,13 +834,37 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.1.0", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -634,7 +878,7 @@ version = "10.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1e83c32c3f3c33b08496e0d1df9ea8c64d39adb8eb36a1ebb1440c690697aef" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "clipboard-win", "dirs-next", @@ -663,6 +907,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + [[package]] name = "serde" version = "1.0.160" @@ -709,7 +965,10 @@ dependencies = [ name = "shiritori" version = "0.1.0" dependencies = [ + "chrono", + "derive_more", "jisho", + "rusqlite", "serde", "serde_json", "simple-websockets", @@ -799,6 +1058,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.40" @@ -819,6 +1087,17 @@ dependencies = [ "syn 2.0.13", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -957,6 +1236,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -974,6 +1259,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1050,12 +1341,30 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 6469755..7eefd21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = { version = "0.4.24", features = ["serde"] } +derive_more = "0.99.17" jisho = "0.1.5" +rusqlite = { version = "0.29.0", features = ["chrono", "bundled"] } serde = { version = "1.0.160", features = ["serde_derive"] } serde_json = "1.0.95" simple-websockets = "0.1.5" diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..59563be --- /dev/null +++ b/src/database.rs @@ -0,0 +1,52 @@ +use crate::Word; + +use derive_more::From; +use std::path::PathBuf; +use rusqlite::{Connection, Result, params}; + +pub struct Database { + conn: Connection, +} + +#[derive(From, Debug)] +pub enum DatabaseCreationError { + RusqliteError(rusqlite::Error), + IoError(std::io::Error), +} + +impl Database { + pub fn new( + testing: bool, + ) -> Result { + let conn = if testing { + Connection::open_in_memory() + } else { + let path = PathBuf::from("shiritori.db"); + //fs::create_dir_all(path.parent().unwrap())?; + Connection::open(path) + }?; + conn.execute( + "CREATE TABLE IF NOT EXISTS word ( + id INTEGER PRIMARY KEY, + word TEXT, reading TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + )", + params![], + )?; + Ok(Self { conn }) + } + + pub fn load_words_before(&self, before_id: i64) -> Result> { + self.conn + .prepare("SELECT id, word, reading, timestamp FROM word WHERE id < ? DESC LIMIT 10")? + .query_map(params![before_id], |row| { + Ok(Word { + id: row.get(0)?, + word: row.get(1)?, + reading: row.get(2)?, + timestamp: row.get(3)?, + }) + })? + .collect::>>() + } +} diff --git a/src/dictionary.rs b/src/dictionary.rs new file mode 100644 index 0000000..f69bfff --- /dev/null +++ b/src/dictionary.rs @@ -0,0 +1,20 @@ +use wana_kana::{ConvertJapanese, IsJapaneseStr}; + +pub fn lookup(input: &str) -> Option<&jisho::Entry> { + let input = input.trim(); + for word in jisho::lookup(input) { + if + // If input has no kanji, + // we can just compare the input to the reading verbatim + // ensuring both are hiragana + (input.is_kana() && input.to_hiragana() == word.reading.to_hiragana()) || + // Otherwise, we have to ensure that the input + // is verbosely the same. + // However, this will cause problems for some words. + // For example, 照り焼き will be accepted but 照焼 won't. + (input == word.kanji) { + return Some(word); + } + } + return None; +} diff --git a/src/main.rs b/src/main.rs index f1a830f..01dc3f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,177 +1,18 @@ -use simple_websockets::{Event, Responder, Message}; -use std::collections::HashMap; -use serde::Serialize; -use wana_kana::{ConvertJapanese, IsJapaneseStr}; +mod database; +pub use database::*; -#[derive(Serialize)] -struct MessageResponse { - event: String, - data: MessageResponseData, -} - -impl MessageResponse { - fn to_message(&self) -> Message { - Message::Text(serde_json::to_string(&self).unwrap()) - } -} - -#[derive(Serialize)] -#[serde(untagged)] -enum MessageResponseData { - Greeting { - id: u64, - }, - Word { - author: u64, - word: String, - reading: Option, - next_char: char, - }, - PlayerCount { - players: u64, - }, - Error { - message: String, - }, -} - -impl MessageResponseData { - fn get_name(&self) -> String { - String::from(match self { - Self::Greeting { .. } => "greeting", - Self::Word { .. } => "word", - Self::PlayerCount { .. } => "playerCount", - Self::Error { .. } => "error", - }) - } - - fn to_response(self) -> MessageResponse { - MessageResponse { - event: self.get_name(), - data: self, - } - } +mod dictionary; +pub use dictionary::*; - fn to_message(self) -> Message { - self.to_response().to_message() - } -} - -fn broadcast_player_count(clients: &mut HashMap) { - let response = MessageResponseData::PlayerCount { players: clients.len() as u64 }.to_response(); - for (_client, responder) in clients.iter() { - responder.send(response.to_message()); - } -} +mod server; +pub use server::*; -fn lookup(input: &str) -> Option<&jisho::Entry> { - let input = input.trim(); - for word in jisho::lookup(input) { - if - // If input has no kanji, - // we can just compare the input to the reading verbatim - // ensuring both are hiragana - (input.is_kana() && input.to_hiragana() == word.reading.to_hiragana()) || - // Otherwise, we have to ensure that the input - // is verbosely the same. - // However, this will cause problems for some words. - // For example, 照り焼き will be accepted but 照焼 won't. - (input == word.kanji) { - return Some(word); - } - } - return None; -} +mod word; +pub use word::*; fn main() { - let event_hub = simple_websockets::launch(8080) - .expect("failed to listen on port 8080"); - let mut clients: HashMap = HashMap::new(); - let mut next_char: Option = None; - let mut last_response: Option = None; - let mut last_client_id: Option = None; - loop { - match event_hub.poll_event() { - Event::Connect(client_id, responder) => { - println!("A client connected with id #{}", client_id); - responder.send(MessageResponseData::Greeting { id: client_id }.to_message()); - if let Some(ref last_response) = last_response { - responder.send(last_response.to_message()); - } - clients.insert(client_id, responder); - broadcast_player_count(&mut clients); - }, - Event::Disconnect(client_id) => { - println!("Client #{} disconnected.", client_id); - clients.remove(&client_id); - broadcast_player_count(&mut clients); - }, - Event::Message(client_id, message) => { - // Ignore binary messages - let message = match message { - Message::Text(message) => message, - Message::Binary(_) => return, - }; - - // Debug - println!("Received a message from client #{}: {:?}", client_id, message); - - let response = if Some(client_id) == last_client_id { - MessageResponseData::Error { - message: String::from("It's not your turn!"), - } - } else { - match lookup(&message) { - Some(entry) => { - if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "ん" { - MessageResponseData::Error { - message: String::from("Can't end with ん!"), - } - } else if next_char.is_none() || entry.reading.chars().next().unwrap().to_string().to_hiragana().chars().next().unwrap() == next_char.unwrap() { - next_char = { - // If final character is lengthener or not kana - // Use semifinal - let mut final_chars = entry.reading.chars().rev(); - let final_char = final_chars.next().unwrap(); - Some(if final_char == 'ー' || !final_char.to_string().is_kana() { - final_chars.next().unwrap() - } else { - final_char - }.to_string().to_hiragana().chars().next().unwrap()) - }; - MessageResponseData::Word { - author: client_id, - word: entry.kanji.to_owned(), - reading: Some(entry.reading.to_owned()), - next_char: next_char.unwrap(), - } - } else { - MessageResponseData::Error { - message: String::from("Wrong starting kana!"), - } - } - } - None => MessageResponseData::Error { - message: String::from("Not in dictionary!"), - }, - } - }.to_response(); - - match response.data { - // Send errors to only this client - MessageResponseData::Error { .. } => { - clients.get(&client_id).unwrap().send(response.to_message()); - }, - // Broadcast everything else to all clients - _ => { - for (_client, responder) in clients.iter() { - responder.send(response.to_message()); - } - last_response = Some(response); - last_client_id = Some(client_id); - }, - } - }, - } - } + const PORT: u16 = 8080; + let mut server = Server::new(PORT) + .expect(&format!("Failed to start server at port {PORT}")); + server.run(); } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..27fa31e --- /dev/null +++ b/src/server.rs @@ -0,0 +1,181 @@ +use crate::dictionary::lookup; + +use simple_websockets::{Event, Responder, Message, EventHub}; +use std::collections::HashMap; +use serde::Serialize; +use wana_kana::{ConvertJapanese, IsJapaneseStr}; + +#[derive(Serialize)] +struct MessageResponse { + event: String, + data: MessageResponseData, +} + +impl MessageResponse { + fn to_message(&self) -> Message { + Message::Text(serde_json::to_string(&self).unwrap()) + } +} + +#[derive(Serialize)] +#[serde(untagged)] +enum MessageResponseData { + Greeting { + id: u64, + }, + Word { + author: u64, + word: String, + reading: Option, + next_char: char, + }, + PlayerCount { + players: u64, + }, + Error { + message: String, + }, +} + +impl MessageResponseData { + fn get_name(&self) -> String { + String::from(match self { + Self::Greeting { .. } => "greeting", + Self::Word { .. } => "word", + Self::PlayerCount { .. } => "playerCount", + Self::Error { .. } => "error", + }) + } + + fn to_response(self) -> MessageResponse { + MessageResponse { + event: self.get_name(), + data: self, + } + } + + fn to_message(self) -> Message { + self.to_response().to_message() + } +} + +pub struct Server { + event_hub: EventHub, + clients: HashMap, + next_char: Option, + last_response: Option, + last_client_id: Option, +} + +impl Server { + pub fn new(port: u16) -> Result { + Ok(Server { + event_hub: simple_websockets::launch(port)?, + clients: HashMap::new(), + next_char: None, + last_response: None, + last_client_id: None, + }) + } + + pub fn run(&mut self) { + loop { + match self.event_hub.poll_event() { + Event::Connect(client_id, responder) => self.handle_connection(client_id, responder), + Event::Disconnect(client_id) => self.handle_disconnection(client_id), + Event::Message(client_id, message) => self.handle_message(client_id, message), + } + } + } + + fn broadcast_player_count(&self) { + let response = MessageResponseData::PlayerCount { players: self.clients.len() as u64 }.to_response(); + for (_client, responder) in self.clients.iter() { + responder.send(response.to_message()); + } + } + + fn handle_connection(&mut self, client_id: u64, responder: Responder) { + println!("A client connected with id #{}", client_id); + responder.send(MessageResponseData::Greeting { id: client_id }.to_message()); + if let Some(ref last_response) = self.last_response { + responder.send(last_response.to_message()); + } + self.clients.insert(client_id, responder); + self.broadcast_player_count(); + } + + fn handle_disconnection(&mut self, client_id: u64) { + println!("Client #{} disconnected.", client_id); + self.clients.remove(&client_id); + self.broadcast_player_count(); + } + + fn handle_message(&mut self, client_id: u64, message: Message) { + // Ignore binary messages + let message = match message { + Message::Text(message) => message, + Message::Binary(_) => return, + }; + + // Debug + println!("Received a message from client #{}: {:?}", client_id, message); + + let response = if Some(client_id) == self.last_client_id { + MessageResponseData::Error { + message: String::from("It's not your turn!"), + } + } else { + match lookup(&message) { + Some(entry) => { + if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "ん" { + MessageResponseData::Error { + message: String::from("Can't end with ん!"), + } + } else if self.next_char.is_none() || entry.reading.chars().next().unwrap().to_string().to_hiragana().chars().next().unwrap() == self.next_char.unwrap() { + self.next_char = { + // If final character is lengthener or not kana + // Use semifinal + let mut final_chars = entry.reading.chars().rev(); + let final_char = final_chars.next().unwrap(); + Some(if final_char == 'ー' || !final_char.to_string().is_kana() { + final_chars.next().unwrap() + } else { + final_char + }.to_string().to_hiragana().chars().next().unwrap()) + }; + MessageResponseData::Word { + author: client_id, + word: entry.kanji.to_owned(), + reading: Some(entry.reading.to_owned()), + next_char: self.next_char.unwrap(), + } + } else { + MessageResponseData::Error { + message: String::from("Wrong starting kana!"), + } + } + } + None => MessageResponseData::Error { + message: String::from("Not in dictionary!"), + }, + } + }.to_response(); + + match response.data { + // Send errors to only this client + MessageResponseData::Error { .. } => { + self.clients.get(&client_id).unwrap().send(response.to_message()); + }, + // Broadcast everything else to all clients + _ => { + for (_client, responder) in self.clients.iter() { + responder.send(response.to_message()); + } + self.last_response = Some(response); + self.last_client_id = Some(client_id); + }, + } + + } +} diff --git a/src/word.rs b/src/word.rs new file mode 100644 index 0000000..08bc818 --- /dev/null +++ b/src/word.rs @@ -0,0 +1,11 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Word { + pub id: i64, + pub word: String, + pub reading: String, + pub timestamp: DateTime, +}