Implement working database
This commit is contained in:
parent
8427eb082b
commit
4076a4c556
5 changed files with 110 additions and 32 deletions
|
@ -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 : `<ruby>${word}<rp>(</rp><rt>${reading}<rt/><rp>)</rp></ruby>`) : 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 : `<ruby>${data.word}<rp>(</rp><rt>${data.reading}<rt/><rp>)</rp></ruby>`) : 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.`;
|
||||
|
|
|
@ -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::<Result<Vec<i64>>>()?
|
||||
.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<Vec<Word>> {
|
||||
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::<Result<Vec<Word>>>()
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
word: Word,
|
||||
next_char: char,
|
||||
},
|
||||
History {
|
||||
words: Vec<Word>,
|
||||
},
|
||||
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<u64, Responder>,
|
||||
next_char: Option<char>,
|
||||
last_response: Option<MessageResponse>,
|
||||
last_client_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[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<Self, simple_websockets::Error> {
|
||||
pub fn new(port: u16, testing: bool) -> Result<Self, ServerCreationError> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
19
src/word.rs
19
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<i64>,
|
||||
pub word: String,
|
||||
pub reading: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl From<Entry> for Word {
|
||||
fn from(entry: Entry) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
word: entry.kanji,
|
||||
reading: entry.reading,
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue