From: Geoffrey Allott Date: Sat, 27 May 2023 16:43:15 +0000 (+0100) Subject: add timestamp filter "created_in_last" X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=8f661e06d38716c0e9ac8bfca409e8b5e2f05a96;p=pokerwave.git add timestamp filter "created_in_last" --- diff --git a/src/client.rs b/src/client.rs index 26df261..9df6985 100644 --- a/src/client.rs +++ b/src/client.rs @@ -43,11 +43,15 @@ impl ConnectionState { match update { ClientInterest::GameList => match &mut self.client { ClientState::LoggedIn { state: LoggedInState::InLobby { ref mut game_list }, .. } => { + let timestamp = match self.server.now().await { + Ok(timestamp) => timestamp, + Err(err) => return once(async move { ServerMessage::InternalError { reason: err.to_string() } }).boxed(), + }; let from = game_list.games_len(); match self.server.game_list(from).await { Ok(games) => { game_list.update(games.len()); - let games: Vec<_> = games.into_iter().filter(|game| game_list.matches(game)).collect(); + let games: Vec<_> = games.into_iter().filter(|game| game_list.matches(game, timestamp)).collect(); iter(games).map(|game| ServerMessage::NewGame { game }).boxed() } Err(err) => once(async move { ServerMessage::ProtocolError { reason: err.to_string() } }).boxed(), @@ -135,10 +139,14 @@ impl ConnectionState { Ok(game_list) => game_list, Err(reason) => return ServerMessage::JoinLobbyFailure { reason }, }; + let timestamp = match self.server.now().await { + Ok(timestamp) => timestamp, + Err(err) => return ServerMessage::InternalError { reason: err.to_string() }, + }; match self.server.game_list(0).await { Ok(games) => { game_list.update(games.len()); - let games = games.into_iter().filter(|game| game_list.matches(game)).collect(); + let games = games.into_iter().filter(|game| game_list.matches(game, timestamp)).collect(); self.client = ClientState::LoggedIn { username, state: LoggedInState::InLobby { game_list } }; ServerMessage::JoinLobbySuccess { filter, games } } diff --git a/src/game/filter.rs b/src/game/filter.rs index 813a316..17002b4 100644 --- a/src/game/filter.rs +++ b/src/game/filter.rs @@ -1,18 +1,22 @@ +use std::str::FromStr; + use nom::{ branch::alt, bytes::complete::{tag, take_until}, - character::complete::{alpha1, multispace0}, - combinator::{map, value}, + character::complete::{alpha1, multispace0, one_of}, + combinator::{map, map_res, recognize, value}, error::ParseError, - multi::{many0_count, separated_list0, separated_list1}, + multi::{many0_count, many1, separated_list0, separated_list1}, sequence::{delimited, preceded, tuple}, IResult, }; use super::{GameSettings, GameSummary}; +use crate::util::timestamp::Timestamp; + pub trait Filter { - fn matches(&self, summary: &GameSummary) -> bool; + fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool; } fn ws<'a, F: 'a, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> @@ -27,11 +31,12 @@ enum Field { Format(String), Title(String), Completed(bool), + CreatedInLast(i64), Parens(Box), } impl Filter for Field { - fn matches(&self, summary: &GameSummary) -> bool { + fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool { match self { Field::Format(format) => match summary.settings { GameSettings::Chatroom(_) => format.eq_ignore_ascii_case("Chatroom"), @@ -40,19 +45,48 @@ impl Filter for Field { }, Field::Title(title) => summary.settings.title().to_lowercase().contains(&title.to_lowercase()), Field::Completed(completed) => summary.status.completed == *completed, - Field::Parens(filter) => filter.matches(summary), + Field::CreatedInLast(millis) => now.millis_since(summary.status.created) <= *millis, + Field::Parens(filter) => filter.matches(summary, now), } } } +fn parse_millis(input: &str) -> IResult<&str, i64> { + let int = || map_res(recognize(many1(one_of("0123456789"))), i64::from_str); + let seconds = map(int(), |secs| secs * 1000); + let minutes_seconds = map( + tuple(( + int(), + tag(":"), + map_res(recognize(one_of("012345")), i64::from_str), + map_res(recognize(one_of("0123456789")), i64::from_str) + )), + |(mins, _, tens, secs)| (mins * 60 + tens * 10 + secs) * 1000 + ); + let hours_minutes_seconds = map( + tuple(( + int(), + tag(":"), + map_res(recognize(one_of("012345")), i64::from_str), + map_res(recognize(one_of("0123456789")), i64::from_str), + tag(":"), + map_res(recognize(one_of("012345")), i64::from_str), + map_res(recognize(one_of("0123456789")), i64::from_str) + )), + |(hours, _, tenmins, mins, _, tens, secs)| (hours * 60 * 60 + tenmins * 10 * 60 + mins * 60 + tens * 10 + secs) * 1000 + ); + alt((hours_minutes_seconds, minutes_seconds, seconds))(input) +} + fn parse_field(input: &str) -> IResult<&str, Field> { let string = || map(alt((alpha1, delimited(tag("\""), take_until("\""), tag("\"")))), String::from); let boolean = || alt((value(true, tag("true")), value(false, tag("false")))); 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 completed = map(preceded(tuple((tag("completed"), ws(tag(":")))), boolean()), Field::Completed); + let created = map(preceded(tuple((tag("created_in_last"), ws(tag(":")))), parse_millis), Field::CreatedInLast); let parens = delimited(tag("("), map(map(ws(parse_or), Box::new), Field::Parens), tag(")")); - alt((format, title, completed, parens))(input) + alt((format, title, completed, created, parens))(input) } #[derive(Clone, Debug, PartialEq, Eq)] @@ -62,8 +96,8 @@ struct Not { } impl Filter for Not { - fn matches(&self, summary: &GameSummary) -> bool { - self.negate ^ self.field.matches(summary) + fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool { + self.negate ^ self.field.matches(summary, now) } } @@ -75,8 +109,8 @@ fn parse_not(input: &str) -> IResult<&str, Not> { struct And(Vec); impl Filter for And { - fn matches(&self, summary: &GameSummary) -> bool { - self.0.iter().all(|filter| filter.matches(summary)) + fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool { + self.0.iter().all(|filter| filter.matches(summary, now)) } } @@ -88,8 +122,8 @@ fn parse_and(input: &str) -> IResult<&str, And> { struct Or(Vec); impl Filter for Or { - fn matches(&self, summary: &GameSummary) -> bool { - self.0.iter().any(|filter| filter.matches(summary)) + fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool { + self.0.iter().any(|filter| filter.matches(summary, now)) } } @@ -107,8 +141,8 @@ impl ParsedFilter { } impl Filter for ParsedFilter { - fn matches(&self, summary: &GameSummary) -> bool { - self.0.matches(summary) + fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool { + self.0.matches(summary, now) } } @@ -120,6 +154,13 @@ pub fn parse_filter(input: &str) -> Result { mod tests { use super::*; + use serde::Deserialize; + use serde::de::value::{Error, I64Deserializer}; + + fn now() -> Timestamp { + Timestamp::deserialize(I64Deserializer::::new(1685204312000)).unwrap() + } + #[test] fn field_parse() { assert_eq!(Field::Format(String::from("KnockOutWhist")), parse_field("format: KnockOutWhist").unwrap().1); @@ -155,11 +196,11 @@ mod tests { ]"#, ) .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])); + assert!(filter.matches(&games[0], now())); + assert!(filter.matches(&games[1], now())); + assert!(!filter.matches(&games[2], now())); + assert!(!filter.matches(&games[3], now())); + assert!(filter.matches(&games[4], now())); } #[test] @@ -175,10 +216,44 @@ mod tests { ]"#, ) .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])); + assert!(filter.matches(&games[0], now())); + assert!(filter.matches(&games[1], now())); + assert!(filter.matches(&games[2], now())); + assert!(filter.matches(&games[3], now())); + assert!(filter.matches(&games[4], now())); + } + + #[test] + fn created_in_last_ten_hours() { + let filter1 = parse_filter("created_in_last: 10:00:00").unwrap(); + let filter2 = parse_filter("created_in_last: 36000").unwrap(); + let filter3 = parse_filter("created_in_last: 600:00").unwrap(); + let games: Vec = serde_json::from_str( + r#"[ + {"id": 0, "status":{"created":1685168302000,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}}, + {"id": 1, "status":{"created":1685168311999,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}}, + {"id": 2, "status":{"created":1685168312000,"completed":false,"players":[]}, "settings": {"format":"Chatroom","title":"Game 2"}}, + {"id": 3, "status":{"created":1685204312000,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}}, + {"id": 4, "status":{"created":1685204312000,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}} + ]"#, + ) + .unwrap(); + assert!(!filter1.matches(&games[0], now())); + assert!(!filter1.matches(&games[1], now())); + assert!(filter1.matches(&games[2], now())); + assert!(filter1.matches(&games[3], now())); + assert!(filter1.matches(&games[4], now())); + + assert!(!filter2.matches(&games[0], now())); + assert!(!filter2.matches(&games[1], now())); + assert!(filter2.matches(&games[2], now())); + assert!(filter2.matches(&games[3], now())); + assert!(filter2.matches(&games[4], now())); + + assert!(!filter3.matches(&games[0], now())); + assert!(!filter3.matches(&games[1], now())); + assert!(filter3.matches(&games[2], now())); + assert!(filter3.matches(&games[3], now())); + assert!(filter3.matches(&games[4], now())); } } diff --git a/src/game/mod.rs b/src/game/mod.rs index 3736c56..5e833d9 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -96,8 +96,8 @@ impl GameList { self.games_len += new_games; } - pub fn matches(&self, game: &GameSummary) -> bool { - self.filter.matches(game) + pub fn matches(&self, game: &GameSummary, now: Timestamp) -> bool { + self.filter.matches(game, now) } }