From: Geoffrey Allott Date: Sat, 13 Mar 2021 22:20:49 +0000 (+0000) Subject: implement game list filtering X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=82beeb4fd050185f0ec87f9f6fc730456a7baea4;p=pokerwave.git implement game list filtering --- diff --git a/Cargo.lock b/Cargo.lock index 50c7c4d..6a99a0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1273,6 +1273,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "sha2", "signal-hook", "signal-hook-async-std", "tide", diff --git a/Cargo.toml b/Cargo.toml index d84cea7..05c09b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ redis = { version = "0.20", features = ["async-std-tls-comp"] } serde = "1" serde_derive = "1" serde_json = "1" +sha2 = "0.9" signal-hook = "0.3" signal-hook-async-std = "0.2" tide = { version = "0.16.0", default-features = false, features = ["h1-server"] } diff --git a/site/img/chatroom.svg b/site/img/chatroom.svg new file mode 100644 index 0000000..5547b38 --- /dev/null +++ b/site/img/chatroom.svg @@ -0,0 +1,8 @@ + + + + A + ♢ + + + diff --git a/site/modules/gamelist.js b/site/modules/gamelist.js index 193e43c..ca0ca99 100644 --- a/site/modules/gamelist.js +++ b/site/modules/gamelist.js @@ -23,6 +23,7 @@ export class GameList { game_list_outside.append(game_list_new); this.game_list_filter_input = document.createElement("input"); this.game_list_filter_input.value = filter; + this.game_list_filter_input.onchange = () => this.send({type: "JoinLobby", filter: this.game_list_filter_input.value}); game_list_outside.append(this.game_list_filter_input); this.container.append(game_list_outside); @@ -165,7 +166,7 @@ export class GameList { format_row.append(format_title); const format_value = document.createElement("td"); const format_input = document.createElement("select"); - for (const format of ["KnockOutWhist", "TexasHoldEm"]) { + for (const format of ["KnockOutWhist", "TexasHoldEm", "Chatroom"]) { const game_format = document.createElement("option"); game_format.setAttribute("value", format); game_format.innerText = format; diff --git a/site/modules/mainmenu.js b/site/modules/mainmenu.js index b561872..019f666 100644 --- a/site/modules/mainmenu.js +++ b/site/modules/mainmenu.js @@ -30,6 +30,17 @@ export class MainMenu { knock_out_whist.onclick = () => this.send({type: "JoinLobby", filter: "format: KnockOutWhist"}); menu_container.append(knock_out_whist); + const chatroom = document.createElement("div"); + chatroom.classList.add("game-format-icon"); + const chatroom_img = document.createElement("img"); + chatroom_img.setAttribute("src", "img/chatroom.svg"); + const chatroom_text = document.createElement("p"); + chatroom_text.innerText = "Chatroom"; + chatroom.append(chatroom_img); + chatroom.append(chatroom_text); + chatroom.onclick = () => this.send({type: "JoinLobby", filter: "format: Chatroom"}); + menu_container.append(chatroom); + this.container.append(menu_container); } } diff --git a/site/style/game-list.css b/site/style/game-list.css index 03d76b8..8e52758 100644 --- a/site/style/game-list.css +++ b/site/style/game-list.css @@ -77,6 +77,14 @@ display: table-row; } +.game-popup > table.Chatroom tr.game-field { + display: none; +} + +.game-popup > table.Chatroom tr.game-field.Chatroom { + display: table-row; +} + .game-popup input, .game-popup select { width: 100%; height: 100%; diff --git a/src/auth.rs b/src/auth.rs index 92db48e..89a9f48 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,8 +1,15 @@ +use sha2::{Digest, Sha256}; + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "method")] pub enum Auth { NoLogin, Plain { password: String }, + Sha256 { + salt: String, + #[serde(with = "hex")] + hash: [u8; 32], + }, } impl Auth { @@ -10,6 +17,12 @@ impl Auth { match self { Auth::NoLogin => false, Auth::Plain { password } => signature == password, + Auth::Sha256 { salt, hash } => { + let mut hasher = Sha256::new(); + hasher.update(salt.as_bytes()); + hasher.update(signature.as_bytes()); + hasher.finalize()[..] == hash[..] + } } } } diff --git a/src/client.rs b/src/client.rs index 0169278..850980d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -46,9 +46,7 @@ impl ConnectionState { let from = game_list.games_len(); match self.server.game_list(from).await { Ok(games) => { - for game in games.clone() { - game_list.push(game); - } + game_list.update(games.len()); iter(games).map(|game| ServerMessage::NewGame { game }).boxed() } Err(err) => once(async move { ServerMessage::ProtocolError { reason: err.to_string() } }).boxed(), @@ -132,12 +130,14 @@ impl ConnectionState { } } (&mut ClientState::LoggedIn { username, .. }, ClientMessage::JoinLobby { filter }) => { - let mut game_list = GameList::new(filter.clone()); + let mut game_list = match GameList::new(filter.clone()) { + Ok(game_list) => game_list, + Err(reason) => return ServerMessage::JoinLobbyFailure { reason }, + }; match self.server.game_list(0).await { Ok(games) => { - for game in games.clone() { - game_list.push(game); - } + game_list.update(games.len()); + let games = games.into_iter().filter(|game| game_list.matches(game)).collect(); self.client = ClientState::LoggedIn { username, state: LoggedInState::InLobby { game_list } }; ServerMessage::JoinLobbySuccess { filter, games } } diff --git a/src/dealer.rs b/src/dealer.rs index 61ba1ca..44a96c6 100644 --- a/src/dealer.rs +++ b/src/dealer.rs @@ -151,15 +151,15 @@ pub async fn spawn_dealers(server: Server, partition: Partition) -> Result<(), s let mut interests = HashSet::new(); interests.insert(ClientInterest::GameList); server_state.register_interests(interests).await; - let mut game_list = GameList::new("".to_string()); + let mut game_list = GameList::match_all(); loop { let games_len = game_list.games_len(); match server_state.game_list(games_len).await { Ok(games) => { + game_list.update(games.len()); for game in games { info!("Starting new game {:?}", game); let id = game.id(); - game_list.push(game); if partition.start_dealer_for(id) { let (server_state, update_stream) = server.new_state().await; if let Ok(dealer) = Dealer::new(server_state, id).await { diff --git a/src/filter.rs b/src/filter.rs deleted file mode 100644 index eddcb49..0000000 --- a/src/filter.rs +++ /dev/null @@ -1,89 +0,0 @@ -use nom::{ - branch::alt, - bytes::complete::{tag, take_until}, - character::complete::{alpha1, multispace0}, - combinator::map, - error::ParseError, - multi::{many0_count, separated_list1}, - sequence::{delimited, preceded, tuple}, - IResult, -}; - -fn ws<'a, F: 'a, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> -where - F: FnMut(&'a str) -> IResult<&'a str, O, E>, -{ - delimited(multispace0, inner, multispace0) -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Field<'a> { - Format(&'a str), - Title(&'a str), - Parens(Box>), -} - -fn parse_field(input: &str) -> IResult<&str, Field> { - let string = || alt((alpha1, delimited(tag("\""), take_until("\""), tag("\"")))); - let format = map(preceded(tuple((tag("format"), ws(tag(":")))), string()), Field::Format); - let title = map(preceded(tuple((tag("title"), ws(tag(":")))), string()), Field::Title); - let parens = delimited(tag("("), map(map(ws(parse_filter), Box::new), Field::Parens), tag(")")); - alt((format, title, parens))(input) -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Not<'a> { - negate: bool, - field: Field<'a>, -} - -fn parse_not(input: &str) -> IResult<&str, Not> { - map(tuple((many0_count(ws(tag("not"))), parse_field)), |(count, field)| Not { negate: count % 2 == 1, field })(input) -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct And<'a>(Vec>); - -fn parse_and(input: &str) -> IResult<&str, And> { - map(separated_list1(ws(tag("and")), parse_not), And)(input) -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Or<'a>(Vec>); - -fn parse_or(input: &str) -> IResult<&str, Or> { - map(separated_list1(ws(tag("or")), parse_and), Or)(input) -} - -type Filter<'a> = Or<'a>; - -fn parse_filter(input: &str) -> IResult<&str, Filter> { - parse_or(input) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn field_parse() { - assert_eq!(Field::Format("KnockOutWhist"), parse_field("format: KnockOutWhist").unwrap().1); - assert_eq!(Field::Title("Superb Game"), parse_field("title: \"Superb Game\"").unwrap().1); - } - - #[test] - fn filter_parse() { - let expected = Or(vec![ - And(vec![Not { - negate: false, - field: Field::Parens(Box::new(Or(vec![And(vec![ - Not { negate: false, field: Field::Format("KnockOutWhist") }, - Not { negate: true, field: Field::Title("Superb Game") }, - ])]))), - }]), - And(vec![Not { negate: false, field: Field::Format("TexasHoldEm") }]), - ]); - let actual = parse_filter("(format: KnockOutWhist and not title: \"Superb Game\") or format: TexasHoldEm").unwrap().1; - assert_eq!(expected, actual); - } -} diff --git a/src/game/chatroom.rs b/src/game/chatroom.rs index 02d7467..ea19346 100644 --- a/src/game/chatroom.rs +++ b/src/game/chatroom.rs @@ -26,6 +26,12 @@ pub struct ChatroomSettings { title: String, } +impl ChatroomSettings { + pub fn title(&self) -> &str { + &self.title + } +} + #[derive(Debug, Clone)] pub struct Chatroom { id: i64, diff --git a/src/game/filter.rs b/src/game/filter.rs new file mode 100644 index 0000000..74658d5 --- /dev/null +++ b/src/game/filter.rs @@ -0,0 +1,182 @@ +use nom::{ + branch::alt, + bytes::complete::{tag, take_until}, + character::complete::{alpha1, multispace0}, + combinator::map, + error::ParseError, + multi::{many0_count, separated_list0, separated_list1}, + sequence::{delimited, preceded, tuple}, + IResult, +}; + +use super::{GameSettings, GameSummary}; + +pub trait Filter { + fn matches(&self, summary: &GameSummary) -> bool; +} + +fn ws<'a, F: 'a, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> +where + F: FnMut(&'a str) -> IResult<&'a str, O, E>, +{ + delimited(multispace0, inner, multispace0) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum Field { + Format(String), + Title(String), + Parens(Box), +} + +impl Filter for Field { + fn matches(&self, summary: &GameSummary) -> bool { + match self { + Field::Format(format) => match summary.settings { + GameSettings::Chatroom(_) => format.eq_ignore_ascii_case("Chatroom"), + GameSettings::KnockOutWhist(_) => format.eq_ignore_ascii_case("KnockOutWhist"), + GameSettings::TexasHoldEm(_) => format.eq_ignore_ascii_case("TexasHoldEm"), + }, + Field::Title(title) => summary.settings.title().to_lowercase().contains(&title.to_lowercase()), + Field::Parens(filter) => filter.matches(summary), + } + } +} + +fn parse_field(input: &str) -> IResult<&str, Field> { + let string = || map(alt((alpha1, delimited(tag("\""), take_until("\""), tag("\"")))), String::from); + let format = map(preceded(tuple((tag("format"), ws(tag(":")))), string()), Field::Format); + let title = map(preceded(tuple((tag("title"), ws(tag(":")))), string()), Field::Title); + let parens = delimited(tag("("), map(map(ws(parse_or), Box::new), Field::Parens), tag(")")); + alt((format, title, parens))(input) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Not { + negate: bool, + field: Field, +} + +impl Filter for Not { + fn matches(&self, summary: &GameSummary) -> bool { + self.negate ^ self.field.matches(summary) + } +} + +fn parse_not(input: &str) -> IResult<&str, Not> { + map(tuple((many0_count(ws(tag("not"))), parse_field)), |(count, field)| Not { negate: count % 2 == 1, field })(input) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct And(Vec); + +impl Filter for And { + fn matches(&self, summary: &GameSummary) -> bool { + self.0.iter().all(|filter| filter.matches(summary)) + } +} + +fn parse_and(input: &str) -> IResult<&str, And> { + map(separated_list0(ws(tag("and")), parse_not), And)(input) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Or(Vec); + +impl Filter for Or { + fn matches(&self, summary: &GameSummary) -> bool { + self.0.iter().any(|filter| filter.matches(summary)) + } +} + +fn parse_or(input: &str) -> IResult<&str, Or> { + map(ws(separated_list1(ws(tag("or")), parse_and)), Or)(input) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ParsedFilter(Or); + +impl ParsedFilter { + pub fn match_all() -> Self { + ParsedFilter(Or(vec![And(vec![])])) + } +} + +impl Filter for ParsedFilter { + fn matches(&self, summary: &GameSummary) -> bool { + self.0.matches(summary) + } +} + +pub fn parse_filter(input: &str) -> Result { + parse_or(input) + .map(|(_, filter)| ParsedFilter(filter)) + .map_err(|err| err.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn field_parse() { + assert_eq!(Field::Format(String::from("KnockOutWhist")), parse_field("format: KnockOutWhist").unwrap().1); + assert_eq!(Field::Title(String::from("Superb Game")), parse_field("title: \"Superb Game\"").unwrap().1); + } + + #[test] + fn filter_parse() { + let expected = Or(vec![ + And(vec![Not { + negate: false, + field: Field::Parens(Box::new(Or(vec![And(vec![ + Not { negate: false, field: Field::Format(String::from("KnockOutWhist")) }, + Not { negate: true, field: Field::Title(String::from("Superb Game")) }, + ])]))), + }]), + And(vec![Not { negate: false, field: Field::Format(String::from("TexasHoldEm")) }]), + ]); + let actual = parse_or("(format: KnockOutWhist and not title: \"Superb Game\") or format: TexasHoldEm").unwrap().1; + assert_eq!(expected, actual); + } + + #[test] + fn filter_matches() { + let filter = parse_filter("format: KnockOutWhist and (title:Game or title:Play)").unwrap(); + let games: Vec = serde_json::from_str( + r#"[ + {"id": 0, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}}, + {"id": 1, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}}, + {"id": 2, "settings": {"format":"Chatroom","title":"Game 2"}}, + {"id": 3, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}}, + {"id": 4, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}} + ]"#, + ) + .unwrap(); + assert!(filter.matches(&games[0])); + assert!(filter.matches(&games[1])); + assert!(!filter.matches(&games[2])); + assert!(!filter.matches(&games[3])); + assert!(filter.matches(&games[4])); + } + + #[test] + fn empty_filter_matches() { + let filter = parse_filter(" ").unwrap(); + let games: Vec = serde_json::from_str( + r#"[ + {"id": 0, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}}, + {"id": 1, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}}, + {"id": 2, "settings": {"format":"Chatroom","title":"Game 2"}}, + {"id": 3, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}}, + {"id": 4, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}} + ]"#, + ) + .unwrap(); + assert!(filter.matches(&games[0])); + assert!(filter.matches(&games[1])); + assert!(filter.matches(&games[2])); + assert!(filter.matches(&games[3])); + assert!(filter.matches(&games[4])); + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index a115677..3fea22f 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,5 +1,6 @@ mod action; mod chatroom; +mod filter; mod poker; mod whist; @@ -11,6 +12,7 @@ use crate::username::Username; use crate::util::timestamp::Timestamp; use self::chatroom::{Chatroom, ChatroomSettings}; +use self::filter::{parse_filter, Filter, ParsedFilter}; use self::poker::{TexasHoldEm, TexasHoldEmSettings}; use self::whist::{KnockOutWhist, KnockOutWhistSettings}; @@ -59,24 +61,42 @@ pub enum GameSettings { TexasHoldEm(TexasHoldEmSettings), } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "format")] +impl GameSettings { + pub fn title(&self) -> &str { + match self { + GameSettings::Chatroom(settings) => settings.title(), + GameSettings::KnockOutWhist(settings) => settings.title(), + GameSettings::TexasHoldEm(settings) => settings.title(), + } + } +} + +#[derive(Debug, Clone)] pub struct GameList { - filter: String, - games: Vec, + filter: ParsedFilter, + games_len: usize, } impl GameList { - pub fn new(filter: String) -> Self { - Self { filter, games: Vec::new() } + pub fn new(filter: String) -> Result { + let filter = parse_filter(&filter)?; + Ok(Self { filter, games_len: 0 }) + } + + pub fn match_all() -> Self { + Self { filter: ParsedFilter::match_all(), games_len: 0 } } pub fn games_len(&self) -> usize { - self.games.len() + self.games_len + } + + pub fn update(&mut self, new_games: usize) { + self.games_len += new_games; } - pub fn push(&mut self, game: GameSummary) { - self.games.push(game); + pub fn matches(&self, game: &GameSummary) -> bool { + self.filter.matches(&game) } } diff --git a/src/game/poker/holdem.rs b/src/game/poker/holdem.rs index a2f62f5..56796d5 100644 --- a/src/game/poker/holdem.rs +++ b/src/game/poker/holdem.rs @@ -40,6 +40,12 @@ pub struct TexasHoldEmSettings { action_timeout: Option, } +impl TexasHoldEmSettings { + pub fn title(&self) -> &str { + &self.title + } +} + #[derive(Clone, Debug)] pub struct TexasHoldEm { id: i64, diff --git a/src/game/whist.rs b/src/game/whist.rs index dc7d7bc..a383953 100644 --- a/src/game/whist.rs +++ b/src/game/whist.rs @@ -25,6 +25,12 @@ pub struct KnockOutWhistSettings { max_players: u32, } +impl KnockOutWhistSettings { + pub fn title(&self) -> &str { + &self.title + } +} + #[derive(Clone, Debug)] pub struct KnockOutWhist { id: i64, diff --git a/src/main.rs b/src/main.rs index c921544..c4f4e96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,6 @@ mod card; mod client; mod config; mod dealer; -mod filter; mod game; mod pubsub; mod rng;