From 69130e47a8f5f50ed2a3c418137aa394afb884c7 Mon Sep 17 00:00:00 2001 From: Demmie <2e3s19@gmail.com> Date: Sat, 22 Apr 2023 19:05:30 -0400 Subject: [PATCH] Implement window data filtration --- Cargo.lock | 13 ++++--- Cargo.toml | 1 + src/config.rs | 15 ++++++++ src/config/file_config.rs | 18 +++++++++ src/config/filters.rs | 79 +++++++++++++++++++++++++++++++++++++++ src/report_client.rs | 19 +++++++++- 6 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 src/config/filters.rs diff --git a/Cargo.lock b/Cargo.lock index b0c4e81..0be5177 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -223,6 +223,7 @@ dependencies = [ "fern", "gethostname 0.4.1", "log", + "regex", "serde", "serde_default", "serde_json", @@ -1405,9 +1406,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", @@ -1416,9 +1417,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "reqwest" diff --git a/Cargo.toml b/Cargo.toml index 6a11ec3..5dea735 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,4 @@ toml = "0.7.3" dirs = "5.0.0" serde = { version = "1.0.160", features = ["derive"] } serde_default = "0.1.0" +regex = "1.8.1" diff --git a/src/config.rs b/src/config.rs index 5fbece4..f7fa32d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,14 @@ mod defaults; mod file_config; +mod filters; use crate::BoxedError; use clap::{arg, value_parser, Command}; use file_config::FileConfig; use std::{path::PathBuf, time::Duration}; +use self::filters::{Filter, Replacement}; + pub struct Config { pub port: u32, pub host: String, @@ -14,6 +17,7 @@ pub struct Config { pub poll_time_window: Duration, pub idle_bucket_name: String, pub active_window_bucket_name: String, + filters: Vec, } impl Config { @@ -55,6 +59,17 @@ impl Config { poll_time_window: config.client.get_poll_time_window(), idle_bucket_name, active_window_bucket_name, + filters: config.client.filters, }) } + + pub fn window_data_replacement(&self, app_id: &str, title: &str) -> Replacement { + for filter in &self.filters { + if let Some(replacement) = filter.replacement(app_id, title) { + return replacement; + } + } + + Replacement::default() + } } diff --git a/src/config/file_config.rs b/src/config/file_config.rs index 67396d3..1c41d34 100644 --- a/src/config/file_config.rs +++ b/src/config/file_config.rs @@ -9,6 +9,8 @@ use std::{ use crate::{config::defaults, BoxedError}; +use super::filters::Filter; + #[derive(Deserialize, DefaultFromSerde)] pub struct ServerConfig { #[serde(default = "defaults::port")] @@ -18,6 +20,7 @@ pub struct ServerConfig { } #[derive(Deserialize, DefaultFromSerde)] +#[serde(rename_all = "kebab-case")] pub struct ClientConfig { #[serde(default = "defaults::idle_timeout_seconds")] idle_timeout_seconds: u32, @@ -25,6 +28,8 @@ pub struct ClientConfig { poll_time_idle_seconds: u32, #[serde(default = "defaults::poll_time_window_seconds")] poll_time_window_seconds: u32, + #[serde(default)] + pub filters: Vec, } impl ClientConfig { @@ -83,6 +88,19 @@ impl FileConfig { # idle-timeout-seconds={} # poll-time-idle-seconds={} # poll-time-window-seconds={} + +# Add as many filters as needed. The first matching filter stops the replacement. +# There should be at least 1 match field, and at least 1 replace field. +# Matches are case sensitive regular expressions between implici ^ and $, e.g. +# - "." matches 1 any character +# - ".*" matches any number of any characters +# - ".+" matches 1 or more any characters. +# - "word" is an exact match. +# [[awatcher.filters]] +# match-app-id = "navigator" +# match-title = ".*Firefox.*" +# replace-app-id = "firefox" +# replace-title = "Unknown" "#, defaults::port(), defaults::host(), diff --git a/src/config/filters.rs b/src/config/filters.rs new file mode 100644 index 0000000..75591f5 --- /dev/null +++ b/src/config/filters.rs @@ -0,0 +1,79 @@ +use regex::Regex; +use serde::de::Error; +use serde::{Deserialize, Deserializer}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Filter { + #[serde(default)] + #[serde(deserialize_with = "string_to_regex")] + match_app_id: Option, + #[serde(default)] + #[serde(deserialize_with = "string_to_regex")] + match_title: Option, + replace_app_id: Option, + replace_title: Option, +} + +fn string_to_regex<'de, D>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = >::deserialize(d)?; + + if let Some(s) = s { + match format!("^{s}$").parse() { + Ok(regex) => Ok(Some(regex)), + Err(err) => Err(D::Error::custom(err)), + } + } else { + Ok(None) + } +} + +#[derive(Default)] +pub struct Replacement { + pub replace_app_id: Option, + pub replace_title: Option, +} + +impl Filter { + fn is_valid(&self) -> bool { + (self.match_app_id.is_some() || self.match_title.is_some()) + && (self.replace_app_id.is_some() || self.replace_title.is_some()) + } + + fn is_match(&self, app_id: &str, title: &str) -> bool { + if let Some(match_app_id) = &self.match_app_id { + if !match_app_id.is_match(app_id) { + return false; + }; + }; + if let Some(match_title) = &self.match_title { + if !match_title.is_match(title) { + return false; + }; + }; + + true + } + + pub fn replacement(&self, app_id: &str, title: &str) -> Option { + if !self.is_valid() { + return None; + } + + if self.is_match(app_id, title) { + let mut replacement = Replacement::default(); + if let Some(new_app_id) = &self.replace_app_id { + replacement.replace_app_id = Some(new_app_id.to_string()); + } + if let Some(new_title) = &self.replace_title { + replacement.replace_title = Some(new_title.to_string()); + } + Some(replacement) + } else { + None + } + } +} diff --git a/src/report_client.rs b/src/report_client.rs index 7121b0b..72dcfeb 100644 --- a/src/report_client.rs +++ b/src/report_client.rs @@ -49,8 +49,23 @@ impl ReportClient { pub fn send_active_window(&self, app_id: &str, title: &str) -> Result<(), BoxedError> { let mut data = Map::new(); - data.insert("app".to_string(), Value::String(app_id.to_string())); - data.insert("title".to_string(), Value::String(title.to_string())); + let mut data_insert = |k: &str, v: String| data.insert(k.to_string(), Value::String(v)); + + let replacement = self.config.window_data_replacement(app_id, title); + let inserted_app_id = if let Some(new_app_id) = replacement.replace_app_id { + trace!("Replacing app_id by {new_app_id}"); + new_app_id + } else { + app_id.to_string() + }; + let inserted_title = if let Some(new_title) = replacement.replace_title { + trace!("Replacing title of {inserted_app_id} by {new_title}"); + new_title + } else { + title.to_string() + }; + data_insert("app", inserted_app_id); + data_insert("title", inserted_title); let event = AwEvent { id: None, timestamp: Utc::now(),