diff --git a/demo/shiritori.js b/demo/shiritori.js index f1ee92c..aa917f0 100644 --- a/demo/shiritori.js +++ b/demo/shiritori.js @@ -4,6 +4,18 @@ const input = document.querySelector("#shiritori-input"); const players = document.querySelector("#shiritori-players"); let id = null; +function displayWord(_word, end) { + let { word, reading } = _word; + p = document.createElement("p"); + p.innerHTML = reading || word === reading ? (word === "" ? reading : `${word}(${reading})`) : word; + if (end) { + div.append(p); + } else { + div.prepend(p); + } + div.scrollTop = div.offsetHeight; +} + ws.onmessage = e => { const { event, data } = JSON.parse(e.data); switch (event) { @@ -12,14 +24,17 @@ ws.onmessage = e => { break; case "word": let waiting = data.author === id; - p = document.createElement("p"); - p.innerHTML = data.reading || data.word === data.reading ? (data.word === "" ? data.reading : `${data.word}(${data.reading})`) : data.word; - div.appendChild(p); - div.scrollTop = div.offsetHeight; + displayWord(data.word, true); input.placeholder = waiting ? "Waiting for other players..." : `${data.next_char}…`; input.disabled = waiting; if (!waiting) input.focus(); break; + case "history": + console.log(data); + data.words.forEach(word => { + displayWord(word, false); + }) + break; case "playerCount": let otherPlayers = data.players - 1; players.innerHTML = `${otherPlayers === 0 ? "No" : otherPlayers} other player${otherPlayers === 1 ? "" : "s"} online.`; diff --git a/src/database.rs b/src/database.rs index 59563be..23328b9 100644 --- a/src/database.rs +++ b/src/database.rs @@ -6,6 +6,7 @@ use rusqlite::{Connection, Result, params}; pub struct Database { conn: Connection, + pub last_word_id: i64, } #[derive(From, Debug)] @@ -28,17 +29,27 @@ impl Database { conn.execute( "CREATE TABLE IF NOT EXISTS word ( id INTEGER PRIMARY KEY, - word TEXT, reading TEXT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + word TEXT, + reading TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )", params![], )?; - Ok(Self { conn }) + let last_word_id = match conn + .prepare("SELECT id FROM word ORDER BY id DESC LIMIT 1")? + .query_map(params![], |row| row.get(0))? + .collect::>>()? + .first() { + Some(id) => *id, + None => 0, // first database entry is id 1 + }; + Ok(Self { conn, last_word_id }) } pub fn load_words_before(&self, before_id: i64) -> Result> { + println!("{}", before_id); self.conn - .prepare("SELECT id, word, reading, timestamp FROM word WHERE id < ? DESC LIMIT 10")? + .prepare("SELECT id, word, reading, timestamp FROM word WHERE id < ? ORDER BY id DESC LIMIT 10")? .query_map(params![before_id], |row| { Ok(Word { id: row.get(0)?, @@ -49,4 +60,16 @@ impl Database { })? .collect::>>() } + + pub fn add_word(&mut self, word: &Word) -> Result<()> { + self.conn.execute( + "INSERT INTO word (word, reading) VALUES (?1, ?2)", + params![ + word.word, + word.reading, + ], + )?; + self.last_word_id += 1; + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index b51f3f0..4fea4ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ pub use word::*; fn main() { const PORT: u16 = 8080; - let mut server = Server::new(PORT) - .unwrap_or_else(|_| panic!("Failed to start server at port {PORT}")); + let mut server = Server::new(PORT, true) + .unwrap_or_else(|error| panic!("Failed to start server at port {PORT}: {:?}", error)); server.run(); } diff --git a/src/server.rs b/src/server.rs index f7efebb..95109b0 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,9 +1,13 @@ -use crate::dictionary::lookup; +use crate::DatabaseCreationError; +use crate::lookup; +use crate::Word; +use crate::Database; use simple_websockets::{Event, Responder, Message, EventHub}; use std::collections::HashMap; use serde::Serialize; use wana_kana::{ConvertJapanese, IsJapaneseStr}; +use derive_more::From; #[derive(Serialize)] struct MessageResponse { @@ -25,10 +29,12 @@ enum MessageResponseData { }, Word { author: u64, - word: String, - reading: Option, + word: Word, next_char: char, }, + History { + words: Vec, + }, PlayerCount { players: u64, }, @@ -42,6 +48,7 @@ impl MessageResponseData { String::from(match self { Self::Greeting { .. } => "greeting", Self::Word { .. } => "word", + Self::History { .. } => "history", Self::PlayerCount { .. } => "playerCount", Self::Error { .. } => "error", }) @@ -61,29 +68,43 @@ impl MessageResponseData { pub struct Server { event_hub: EventHub, + database: Database, clients: HashMap, next_char: Option, - last_response: Option, last_client_id: Option, } +#[derive(From, Debug)] +pub enum ServerCreationError { + SimpleWebsocketsError(simple_websockets::Error), + DatabaseCreationError(DatabaseCreationError), +} + +#[derive(From, Debug)] +pub enum ServerError { + DatabaseError(rusqlite::Error), +} + impl Server { - pub fn new(port: u16) -> Result { + pub fn new(port: u16, testing: bool) -> Result { Ok(Server { event_hub: simple_websockets::launch(port)?, + database: Database::new(testing)?, 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() { + match 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), + } { + Ok(()) => {}, + Err(error) => println!("{:?}", error), } } } @@ -95,27 +116,31 @@ impl Server { } } - fn handle_connection(&mut self, client_id: u64, responder: Responder) { + fn handle_connection(&mut self, client_id: u64, responder: Responder) -> Result<(), ServerError> { println!("A client connected with id #{}", client_id); - responder.send(MessageResponseData::Greeting { id: client_id }.into_message()); - if let Some(ref last_response) = self.last_response { - responder.send(last_response.to_message()); - } + responder.send(MessageResponseData::Greeting { + id: client_id + }.into_message()); + responder.send(MessageResponseData::History { + words: self.database.load_words_before(self.database.last_word_id + 1)? + }.into_message()); self.clients.insert(client_id, responder); self.broadcast_player_count(); + Ok(()) } - fn handle_disconnection(&mut self, client_id: u64) { + fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> { println!("Client #{} disconnected.", client_id); self.clients.remove(&client_id); self.broadcast_player_count(); + Ok(()) } - fn handle_message(&mut self, client_id: u64, message: Message) { + fn handle_message(&mut self, client_id: u64, message: Message) -> Result<(), ServerError> { // Ignore binary messages let message = match message { Message::Text(message) => message, - Message::Binary(_) => return, + Message::Binary(_) => return Ok(()), }; // Debug @@ -144,10 +169,11 @@ impl Server { final_char }.to_string().to_hiragana().chars().next().unwrap()) }; + let word: Word = entry.clone().into(); + self.database.add_word(&word)?; MessageResponseData::Word { author: client_id, - word: entry.kanji.to_owned(), - reading: Some(entry.reading.to_owned()), + word, next_char: self.next_char.unwrap(), } } else { @@ -172,10 +198,9 @@ impl Server { for (_client, responder) in self.clients.iter() { responder.send(response.to_message()); } - self.last_response = Some(response); self.last_client_id = Some(client_id); }, - } - + }; + Ok(()) } } diff --git a/src/word.rs b/src/word.rs index 08bc818..cfa1701 100644 --- a/src/word.rs +++ b/src/word.rs @@ -1,11 +1,26 @@ use chrono::{DateTime, Utc}; use serde::Serialize; +use jisho::Entry; +use std::convert::From; #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct Word { - pub id: i64, + #[serde(skip_serializing)] + pub id: Option, pub word: String, pub reading: String, - pub timestamp: DateTime, + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option>, +} + +impl From for Word { + fn from(entry: Entry) -> Self { + Self { + id: None, + word: entry.kanji, + reading: entry.reading, + timestamp: None, + } + } }