From 1576bfe3467a80edff5838c14cd56cdce76d72db Mon Sep 17 00:00:00 2001 From: Brayo Date: Thu, 1 May 2025 15:13:08 +0300 Subject: [PATCH] wip --- Cargo.lock | 134 +++++++++++++- aw-client-rust/Cargo.toml | 5 +- aw-client-rust/src/blocking.rs | 1 + aw-client-rust/src/classes.rs | 167 ++++++++++++++++++ aw-client-rust/src/lib.rs | 7 + aw-client-rust/src/queries.rs | 310 +++++++++++++++++++++++++++++++++ aw-models/src/lib.rs | 5 + aw-models/src/settings.rs | 76 ++++++++ 8 files changed, 695 insertions(+), 10 deletions(-) create mode 100644 aw-client-rust/src/classes.rs create mode 100644 aw-client-rust/src/queries.rs create mode 100644 aw-models/src/settings.rs diff --git a/Cargo.lock b/Cargo.lock index c8a3866b..a34bf069 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,6 +206,9 @@ dependencies = [ "aw-server", "chrono", "gethostname", + "log", + "phf", + "rand 0.9.1", "reqwest", "rocket", "serde", @@ -991,7 +994,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -1415,7 +1430,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1660,6 +1675,48 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1765,6 +1822,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -1772,8 +1835,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1783,7 +1856,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1792,7 +1875,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", ] [[package]] @@ -1839,7 +1931,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror", ] @@ -1969,7 +2061,7 @@ dependencies = [ "num_cpus", "parking_lot", "pin-project-lite", - "rand", + "rand 0.8.5", "ref-cast", "rocket_codegen", "rocket_http", @@ -2340,6 +2432,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -2808,7 +2906,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ - "getrandom", + "getrandom 0.2.15", "serde", ] @@ -2855,6 +2953,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.93" @@ -3160,6 +3267,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/aw-client-rust/Cargo.toml b/aw-client-rust/Cargo.toml index f41c3706..8b86550b 100644 --- a/aw-client-rust/Cargo.toml +++ b/aw-client-rust/Cargo.toml @@ -7,11 +7,14 @@ authors = ["Johan Bjäreholt "] [dependencies] reqwest = { version = "0.11", features = ["json", "blocking"] } gethostname = "0.4" -serde = "1.0" +serde = { version = "1.0", features = ["derive"] } +phf = { version = "0.11", features = ["macros"] } serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } aw-models = { path = "../aw-models" } tokio = { version = "1.28.2", features = ["rt"] } +rand = "0.9" +log = "0.4" [dev-dependencies] aw-datastore = { path = "../aw-datastore" } diff --git a/aw-client-rust/src/blocking.rs b/aw-client-rust/src/blocking.rs index ee7822bc..efa62ac8 100644 --- a/aw-client-rust/src/blocking.rs +++ b/aw-client-rust/src/blocking.rs @@ -79,6 +79,7 @@ impl AwClient { proxy_method!(delete_event, (), bucketname: &str, event_id: i64); proxy_method!(get_event_count, i64, bucketname: &str); proxy_method!(get_info, aw_models::Info,); + proxy_method!(get_setting, aw_models::Settings, setting: &str); pub fn wait_for_start(&self) -> Result<(), Box> { self.client.wait_for_start() diff --git a/aw-client-rust/src/classes.rs b/aw-client-rust/src/classes.rs new file mode 100644 index 00000000..68e7d0e1 --- /dev/null +++ b/aw-client-rust/src/classes.rs @@ -0,0 +1,167 @@ +//! Default classes +//! +//! Taken from default classes in aw-webui + +use log::warn; +use rand::Rng; +use serde::{Deserialize, Serialize}; + +use super::blocking::AwClient as ActivityWatchClient; + +pub type CategoryId = Vec; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CategorySpec { + #[serde(rename = "type")] + pub spec_type: String, + pub regex: String, + #[serde(default)] + pub ignore_case: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClassSetting { + pub name: Vec, + pub rule: CategorySpec, +} + +/// Returns the default categorization classes +pub fn default_classes() -> Vec<(CategoryId, CategorySpec)> { + vec![ + ( + vec!["Work".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Google Docs|libreoffice|ReText".to_string(), + ignore_case: false, + }, + ), + ( + vec!["Work".to_string(), "Programming".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "GitHub|Stack Overflow|BitBucket|Gitlab|vim|Spyder|kate|Ghidra|Scite" + .to_string(), + ignore_case: false, + }, + ), + ( + vec![ + "Work".to_string(), + "Programming".to_string(), + "ActivityWatch".to_string(), + ], + CategorySpec { + spec_type: "regex".to_string(), + regex: "ActivityWatch|aw-".to_string(), + ignore_case: true, + }, + ), + ( + vec!["Work".to_string(), "Image".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Gimp|Inkscape".to_string(), + ignore_case: false, + }, + ), + ( + vec!["Work".to_string(), "Video".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Kdenlive".to_string(), + ignore_case: false, + }, + ), + ( + vec!["Work".to_string(), "Audio".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Audacity".to_string(), + ignore_case: false, + }, + ), + ( + vec!["Work".to_string(), "3D".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Blender".to_string(), + ignore_case: false, + }, + ), + ( + vec!["Media".to_string(), "Games".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Minecraft|RimWorld".to_string(), + ignore_case: false, + }, + ), + ( + vec!["Media".to_string(), "Video".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "YouTube|Plex|VLC".to_string(), + ignore_case: false, + }, + ), + ( + vec!["Media".to_string(), "Social Media".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "reddit|Facebook|Twitter|Instagram|devRant".to_string(), + ignore_case: true, + }, + ), + ( + vec!["Media".to_string(), "Music".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Spotify|Deezer".to_string(), + ignore_case: true, + }, + ), + ( + vec!["Comms".to_string(), "IM".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Messenger|Telegram|Signal|WhatsApp|Rambox|Slack|Riot|Discord|Nheko" + .to_string(), + ignore_case: false, + }, + ), + ( + vec!["Comms".to_string(), "Email".to_string()], + CategorySpec { + spec_type: "regex".to_string(), + regex: "Gmail|Thunderbird|mutt|alpine".to_string(), + ignore_case: false, + }, + ), + ] +} + +/// Get classes from server-side settings. +/// Might throw an error if not set yet, in which case we use the default classes as a fallback. +pub fn get_classes() -> Vec<(CategoryId, CategorySpec)> { + let mut rng = rand::rng(); + let random_int = rng.random_range(0..10001); + let client_id = format!("get-setting-{}", random_int); + + // Create a client with a random ID, similar to the Python implementation + let awc = ActivityWatchClient::new("localhost", 5600, &client_id) + .expect("Failed to create ActivityWatch client"); + + awc.get_setting("classes") + .map(|setting| { + // Deserialize the setting into a Vec<(CategoryId, CategorySpec)> + serde_json::from_value( + serde_json::to_value(setting).expect("Failed to convert Settings to Value"), + ) + .unwrap_or_else(|_| default_classes()) + }) + .unwrap_or_else(|_| { + warn!("Failed to get classes from server, using default classes"); + default_classes() + }) +} diff --git a/aw-client-rust/src/lib.rs b/aw-client-rust/src/lib.rs index f57d1422..739b3cc3 100644 --- a/aw-client-rust/src/lib.rs +++ b/aw-client-rust/src/lib.rs @@ -6,6 +6,8 @@ extern crate serde_json; extern crate tokio; pub mod blocking; +pub mod classes; +pub mod queries; use std::{collections::HashMap, error::Error}; @@ -224,6 +226,11 @@ impl AwClient { self.client.get(url).send().await?.json().await } + pub async fn get_setting(&self, setting: &str) -> Result { + let url = format!("{}/api/0/settings/{}", self.baseurl, setting); + self.client.get(url).send().await?.json().await + } + // TODO: make async pub fn wait_for_start(&self) -> Result<(), Box> { let socket_addrs = self.baseurl.socket_addrs(|| None)?; diff --git a/aw-client-rust/src/queries.rs b/aw-client-rust/src/queries.rs new file mode 100644 index 00000000..07d794be --- /dev/null +++ b/aw-client-rust/src/queries.rs @@ -0,0 +1,310 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Browser application names mapped by browser type +pub static BROWSER_APPNAMES: phf::Map<&'static str, &'static [&'static str]> = phf::phf_map! { + "chrome" => &[ + // Chrome + "Google Chrome", + "Google-chrome", + "chrome.exe", + "google-chrome-stable", + // Chromium + "Chromium", + "Chromium-browser", + "Chromium-browser-chromium", + "chromium.exe", + // Pre-releases + "Google-chrome-beta", + "Google-chrome-unstable", + // Brave (should this be merged with the brave entry?) + "Brave-browser", + ], + "firefox" => &[ + "Firefox", + "Firefox.exe", + "firefox", + "firefox.exe", + "Firefox Developer Edition", + "firefoxdeveloperedition", + "Firefox-esr", + "Firefox Beta", + "Nightly", + "org.mozilla.firefox", + ], + "opera" => &["opera.exe", "Opera"], + "brave" => &["brave.exe"], + "edge" => &[ + "msedge.exe", // Windows + "Microsoft Edge", // macOS + ], + "vivaldi" => &["Vivaldi-stable", "Vivaldi-snapshot", "vivaldi.exe"], +}; + +pub const DEFAULT_LIMIT: u32 = 100; + +/// Represents a class rule for categorization +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClassRule { + pub patterns: Vec, + pub properties: HashMap, +} + +/// Base query parameters +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueryParamsBase { + #[serde(default)] + pub bid_browsers: Vec, + #[serde(default)] + pub classes: Vec, + #[serde(default)] + pub filter_classes: Vec>, + #[serde(default = "default_true")] + pub filter_afk: bool, + #[serde(default = "default_true")] + pub include_audible: bool, +} + +fn default_true() -> bool { + true +} + +/// Query parameters specific to desktop +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DesktopQueryParams { + #[serde(flatten)] + pub base: QueryParamsBase, + pub bid_window: String, + pub bid_afk: String, +} + +/// Query parameters specific to Android +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AndroidQueryParams { + #[serde(flatten)] + pub base: QueryParamsBase, + pub bid_android: String, +} + +/// Enum to represent different types of query parameters +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum QueryParams { + Desktop(DesktopQueryParams), + Android(AndroidQueryParams), +} + +impl QueryParams { + /// Build canonical events query string + pub fn canonical_events(&self) -> String { + match self { + QueryParams::Desktop(params) => build_desktop_canonical_events(params), + QueryParams::Android(params) => build_android_canonical_events(params), + } + } +} + +fn build_desktop_canonical_events(params: &DesktopQueryParams) -> String { + let mut query = Vec::new(); + + // Fetch window events + query.push(format!( + "events = flood(query_bucket(find_bucket(\"{}\")))", + escape_doublequote(¶ms.bid_window) + )); + + // Fetch not-afk events + if params.base.filter_afk { + query.push(format!( + "not_afk = flood(query_bucket(find_bucket(\"{}\"))); + not_afk = filter_keyvals(not_afk, \"status\", [\"not-afk\"])", + escape_doublequote(¶ms.bid_afk) + )); + } + + // Add browser events if any browser buckets specified + if !params.base.bid_browsers.is_empty() { + query.push(build_browser_events(params)); + + if params.base.include_audible { + query.push( + "audible_events = filter_keyvals(browser_events, \"audible\", [true]); + not_afk = period_union(not_afk, audible_events)" + .to_string(), + ); + } + } + + // Filter out window events when user was AFK + if params.base.filter_afk { + query.push("events = filter_period_intersect(events, not_afk)".to_string()); + } + + // Add categorization if classes specified + if !params.base.classes.is_empty() { + query.push(format!( + "events = categorize(events, {})", + serde_json::to_string(¶ms.base.classes).unwrap() + )); + } + + // Filter categories if specified + if !params.base.filter_classes.is_empty() { + query.push(format!( + "events = filter_keyvals(events, \"$category\", {})", + serde_json::to_string(¶ms.base.filter_classes).unwrap() + )); + } + + query.join(";\n") +} + +fn build_android_canonical_events(params: &AndroidQueryParams) -> String { + let mut query = Vec::new(); + + // Fetch app events + query.push(format!( + "events = flood(query_bucket(find_bucket(\"{}\")))", + escape_doublequote(¶ms.bid_android) + )); + + // Merge events by app + query.push("events = merge_events_by_keys(events, [\"app\"])".to_string()); + + // Add categorization if classes specified + if !params.base.classes.is_empty() { + query.push(format!( + "events = categorize(events, {})", + serde_json::to_string(¶ms.base.classes).unwrap() + )); + } + + // Filter categories if specified + if !params.base.filter_classes.is_empty() { + query.push(format!( + "events = filter_keyvals(events, \"$category\", {})", + serde_json::to_string(¶ms.base.filter_classes).unwrap() + )); + } + + query.join(";\n") +} + +fn build_browser_events(params: &DesktopQueryParams) -> String { + let mut query = String::from("browser_events = [];\n"); + + for browser_bucket in ¶ms.base.bid_browsers { + for (browser_name, app_names) in BROWSER_APPNAMES.entries() { + if browser_bucket.contains(browser_name) { + query.push_str(&format!( + "events_{0} = flood(query_bucket(\"{1}\")); + window_{0} = filter_keyvals(events, \"app\", {2}); + events_{0} = filter_period_intersect(events_{0}, window_{0}); + events_{0} = split_url_events(events_{0}); + browser_events = concat(browser_events, events_{0}); + browser_events = sort_by_timestamp(browser_events);\n", + browser_name, + escape_doublequote(browser_bucket), + serde_json::to_string(app_names).unwrap() + )); + } + } + } + + query +} + +/// Build a full desktop query +pub fn full_desktop_query(params: &DesktopQueryParams) -> String { + let mut query = QueryParams::Desktop(params.clone()).canonical_events(); + + // Add basic event aggregations + query.push_str(&format!( + " + title_events = sort_by_duration(merge_events_by_keys(events, [\"app\", \"title\"])); + app_events = sort_by_duration(merge_events_by_keys(title_events, [\"app\"])); + cat_events = sort_by_duration(merge_events_by_keys(events, [\"$category\"])); + app_events = limit_events(app_events, {}); + title_events = limit_events(title_events, {}); + duration = sum_durations(events); + ", + DEFAULT_LIMIT, DEFAULT_LIMIT + )); + + // Add browser-specific query parts if browser buckets exist + if !params.base.bid_browsers.is_empty() { + query.push_str(&format!( + " + browser_events = split_url_events(browser_events); + browser_urls = merge_events_by_keys(browser_events, [\"url\"]); + browser_urls = sort_by_duration(browser_urls); + browser_urls = limit_events(browser_urls, {}); + browser_domains = merge_events_by_keys(browser_events, [\"$domain\"]); + browser_domains = sort_by_duration(browser_domains); + browser_domains = limit_events(browser_domains, {}); + browser_duration = sum_durations(browser_events); + ", + DEFAULT_LIMIT, DEFAULT_LIMIT + )); + } else { + query.push_str( + " + browser_events = []; + browser_urls = []; + browser_domains = []; + browser_duration = 0; + ", + ); + } + + // Add return statement + query.push_str( + " + RETURN = { + \"events\": events, + \"window\": { + \"app_events\": app_events, + \"title_events\": title_events, + \"cat_events\": cat_events, + \"active_events\": not_afk, + \"duration\": duration + }, + \"browser\": { + \"domains\": browser_domains, + \"urls\": browser_urls, + \"duration\": browser_duration + } + }; + ", + ); + + query +} + +fn escape_doublequote(s: &str) -> String { + s.replace('\"', "\\\"") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_desktop_query_generation() { + let params = DesktopQueryParams { + base: QueryParamsBase { + bid_browsers: vec![], + classes: vec![], + filter_classes: vec![], + filter_afk: true, + include_audible: true, + }, + bid_window: "aw-watcher-window_".to_string(), + bid_afk: "aw-watcher-afk_".to_string(), + }; + + let query = full_desktop_query(¶ms); + assert!(!query.is_empty()); + assert!(query.contains("events = flood")); + } +} diff --git a/aw-models/src/lib.rs b/aw-models/src/lib.rs index 157f7cfc..4639fa77 100644 --- a/aw-models/src/lib.rs +++ b/aw-models/src/lib.rs @@ -21,6 +21,7 @@ mod duration; mod event; mod info; mod query; +mod settings; mod timeinterval; mod tryvec; @@ -30,5 +31,9 @@ pub use self::bucket::BucketsExport; pub use self::event::Event; pub use self::info::Info; pub use self::query::Query; +pub use self::settings::Settings; +pub use self::settings::{ + Class, ClassData, ClassRule, NewReleaseCheckData, UserSatisfactionPollData, View, ViewElement, +}; pub use self::timeinterval::TimeInterval; pub use self::tryvec::TryVec; diff --git a/aw-models/src/settings.rs b/aw-models/src/settings.rs new file mode 100644 index 00000000..57cbc475 --- /dev/null +++ b/aw-models/src/settings.rs @@ -0,0 +1,76 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub struct NewReleaseCheckData { + pub how_often_to_check: i64, + pub is_enabled: bool, + pub next_check_time: String, + pub times_checked: i32, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub struct UserSatisfactionPollData { + pub is_enabled: bool, + pub next_poll_time: String, + pub times_poll_is_shown: i32, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub struct ViewElement { + pub size: i32, + #[serde(rename = "type")] + pub element_type: String, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub struct View { + pub elements: Vec, + pub id: String, + pub name: String, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub struct ClassData { + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub score: Option, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub struct ClassRule { + #[serde(skip_serializing_if = "Option::is_none")] + pub ignore_case: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub regex: Option, + #[serde(rename = "type")] + pub rule_type: String, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub struct Class { + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + pub id: i32, + pub name: Vec, + pub rule: ClassRule, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct Settings { + pub landing_page: String, + pub start_of_day: String, + pub always_active_pattern: String, + pub new_release_check_data: NewReleaseCheckData, + pub start_of_week: String, + pub use_color_fallback: bool, + pub user_satisfaction_poll_data: UserSatisfactionPollData, + pub request_timeout: i32, + pub devmode: bool, + pub theme: String, + pub views: Vec, + pub duration_default: i32, + pub classes: Vec, + pub initial_timestamp: String, +}