add timestamp filter "created_in_last"
authorGeoffrey Allott <geoffrey@allott.email>
Sat, 27 May 2023 16:43:15 +0000 (17:43 +0100)
committerGeoffrey Allott <geoffrey@allott.email>
Sat, 27 May 2023 16:43:15 +0000 (17:43 +0100)
src/client.rs
src/game/filter.rs
src/game/mod.rs

index 26df2613b32741b189800ef9704f2760551ad1de..9df6985fe3e1aa02ffc82acdeb86bdadb3d95bfc 100644 (file)
@@ -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 }
                     }
index 813a316f73eb7849a666f1464fb43a9dc5749199..17002b4d154c0d259d1b001594515fa5ff31ec9f 100644 (file)
@@ -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<Or>),
 }
 
 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<Not>);
 
 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<And>);
 
 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<ParsedFilter, String> {
 mod tests {
     use super::*;
 
+    use serde::Deserialize;
+    use serde::de::value::{Error, I64Deserializer};
+
+    fn now() -> Timestamp {
+        Timestamp::deserialize(I64Deserializer::<Error>::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<GameSummary> = 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()));
     }
 }
index 3736c56d30b2e3b2c4dd6824aa5be55bdbbfadf1..5e833d916b936573a920c1f3550e58f9be66bbbc 100644 (file)
@@ -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)
     }
 }