diff --git a/Cargo.lock b/Cargo.lock index 2fab129..43bf01c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -161,6 +170,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-tz" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f5ebdc942f57ed96d560a6d1a459bae5851102a25d5bf89dc04ae453e31ecf" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clap" version = "4.4.0" @@ -705,12 +736,59 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.12" @@ -747,6 +825,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "redox_syscall" version = "0.3.5" @@ -756,6 +849,35 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + [[package]] name = "reqwest" version = "0.11.18" @@ -913,6 +1035,12 @@ dependencies = [ "time 0.3.27", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.8" @@ -1036,6 +1164,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "time_tracker" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "clap", + "log", + "reqwest", + "serde", + "serde_json", + "simplelog", + "toml", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 1e7a2f2..a19461f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] members = [ - "status_cloud" + "status_cloud", + "time_tracker" ] diff --git a/flake.nix b/flake.nix index ce041b8..bee77b9 100644 --- a/flake.nix +++ b/flake.nix @@ -69,6 +69,15 @@ rust-project TODO: write shell script for automatically updating `cargoHash` description = "Server status saved to a nextcloud note service."; }; }); + time_tracker = pkgs.rustPlatform.buildRustPackage (rustSettings // { + pname = "time_tracker"; + version = "0.1.0"; + buildAndTestSubdir = "time_tracker"; + cargoHash = "sha256-AlKMHv+1et7ZRWwEhbVLNtMiWaTsVbBBh7luGOr8e3o="; + meta = meta // { + description = "Using nextcloud notes to track time usage."; + }; + }); }; nixosModules.default = { config, ... }: let lib = nixpkgs.lib; @@ -97,6 +106,7 @@ rust-project TODO: write shell script for automatically updating `cargoHash` ''; }; }; + config.systemd.services.status_cloud = let cfg = config.services.status_cloud; pkg = self.packages.${system}.status_cloud; @@ -106,7 +116,7 @@ rust-project TODO: write shell script for automatically updating `cargoHash` serviceConfig = { Type = "oneshot"; ExecStart = '' - ${cfg.package}/bin/status_cloud --config-file ${builtins.toString cfg.config_path} + ${cfg.package}/bin/status_cloud --config-file ${builtins.toString cfg.config_path} ''; }; }; @@ -120,6 +130,54 @@ rust-project TODO: write shell script for automatically updating `cargoHash` timerConfig.OnCalendar = [ "*:0/${builtins.toString cfg.frequency}" ]; }; + + # Time Tracker + options.services.time_tracker = { + enable = lib.mkEnableOption (lib.mdDoc "time tracker service"); + package = lib.mkOption { + type = lib.types.package; + default = self.packages.${system}.time_tracker; + defaultText = "pkgs.time_tracker"; + description = lib.mdDoc '' + The time_tracker package that should be used + ''; + }; + config_path = lib.mkOption { + type = lib.types.path; + descript = lib.mkDoc '' + The file path to the toml that contains user information secrets + ''; + }; + frequency = lib.mkOption { + type = lib.types.int; + description = lib.mdDoc '' + The number of minutes to wait between updates + ''; + }; + }; + + config.systemd.services.time_tracker = let + cfg = config.services.time_tracker; + pkg = self.packages.${system}.time_tracker; + in { + description = "Nextcloud Time Tracker"; + serviceConfig = { + Type = "oneshot"; + ExecStart = '' + ${cfg.package}/bin/time_tracker --config-file ${builtins.toString cfg.config_path} + ''; + }; + }; + + options.systemd.timers.time_tracker = let + cfg = config.services.time_tracker; + pkg = self.packages.${system}.time_tracker; + in lib.mkIf cfg.enable { + wantedBy = [ "timers.target" ]; + partOf = [ "time_tracker.service" ]; + timerConfig.OnCalendar = [ "*:0/${builtins.toString cfg.frequency}" ]; + }; + }; }; } diff --git a/status_cloud/src/main.rs b/status_cloud/src/main.rs index f160bb5..b255b63 100644 --- a/status_cloud/src/main.rs +++ b/status_cloud/src/main.rs @@ -2,7 +2,7 @@ use clap::Parser; use log::{debug, error, warn}; use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; -use std::{process::Command, path::PathBuf}; +use std::{path::PathBuf, process::Command}; fn main() -> Result<(), Box> { let args = CliArgs::parse(); @@ -63,11 +63,7 @@ fn main() -> Result<(), Box> { let mut drive_temps: Vec = vec![]; for drive in drives { - let output = match Command::new("hddtemp") - .arg("--unit=F") - .arg(&drive) - .output() - { + let output = match Command::new("hddtemp").arg("--unit=F").arg(&drive).output() { Ok(val) => String::from_utf8_lossy(&val.stdout).into_owned(), Err(e) => { warn!("Error running hddtemp: {e}"); @@ -89,17 +85,14 @@ fn main() -> Result<(), Box> { Client::new() .put(format!( "https://{}/index.php/apps/notes/api/v1/notes/{}", - &cfg.server_url, - &cfg.note_id + &cfg.server_url, &cfg.note_id )) .header("Accept", "application/json") .header("Content-Type", "application/json") .basic_auth(&cfg.user, Some(&cfg.pswd)) - .body( - serde_json::to_string(&NoteUpdate { - content: body_content, - })?, - ) + .body(serde_json::to_string(&NoteUpdate { + content: body_content, + })?) .send()?; Ok(()) diff --git a/time_tracker/Cargo.toml b/time_tracker/Cargo.toml new file mode 100644 index 0000000..dc464fe --- /dev/null +++ b/time_tracker/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "time_tracker" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.26" +chrono-tz = "0.8.3" +clap = { version = "4.4.0", features = ["derive"] } +log = "0.4.20" +reqwest = { version = "0.11.18", features = ["blocking", "json"] } +serde = { version = "1.0.184", features = ["serde_derive"] } +serde_json = "1.0.105" +simplelog = "0.12.1" +toml = "0.7.6" diff --git a/time_tracker/src/main.rs b/time_tracker/src/main.rs new file mode 100644 index 0000000..66748ac --- /dev/null +++ b/time_tracker/src/main.rs @@ -0,0 +1,225 @@ +use chrono::{DateTime, Utc, Datelike}; +use chrono_tz::US::Central; +use clap::Parser; +use log::{debug, error}; +use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + // --- Loading and setup --- + let args = CliArgs::parse(); + + simplelog::SimpleLogger::init( + match args.debug { + true => simplelog::LevelFilter::Debug, + false => simplelog::LevelFilter::Info, + }, + simplelog::Config::default(), + )?; + + debug!("Opening Config file: {}", args.config_file.display()); + debug!( + "Config file exists: {}", + std::fs::metadata(&args.config_file).is_ok() + ); + + let file_contents = match std::fs::read_to_string(&args.config_file) { + Ok(val) => val, + Err(e) => { + error!("Could not read config file: {e}"); + panic!("{e}"); + } + }; + + let cfg: Config = match toml::from_str(&file_contents) { + Ok(val) => val, + Err(e) => { + error!("Could not parse config file: {e}"); + panic!("{e}"); + } + }; + // --- END Loading and setup --- + + // --- Get checked and unchecked --- + let primary_note = Client::new() + .get(format!( + "https://{}/index.php/apps/notes/api/v1/notes/{}", + &cfg.server_url, &cfg.primary_note_id + )) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .basic_auth(&cfg.user, Some(&cfg.pswd)) + .send()? + .json::()?; + + let primary_note_lines: Vec<&str> = primary_note.content.lines().collect(); + + let mut unchecked: Vec = vec![]; + let mut checked: Vec = vec![]; + + for line in primary_note_lines.iter() { + // if line is a checkbox + if line.starts_with("- [") { + if line.starts_with("- [ ] ") { + unchecked.push(line.replace("- [ ] ", "")); + } else if line.starts_with("- [x] ") { + checked.push(line.replace("- [x] ", "")); + } + } + } + + // --- END Get checked and unchecked --- + + // --- Get current log --- + let logging_note = Client::new() + .get(format!( + "https://{}/index.php/apps/notes/api/v1/notes/{}", + &cfg.server_url, &cfg.logging_note_id + )) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .basic_auth(&cfg.user, Some(&cfg.pswd)) + .send()? + .json::()?; + + let now = Utc::now().with_timezone(&Central); + + let mut body_content: Vec = vec![format!( + "*Last Updated:* {} Central Time", + now.format("%D - %H:%M:%S") + )]; + + let logging_note_lines: Vec<&str> = logging_note.content.lines().collect(); + + for line in logging_note_lines { + if line.starts_with("*Last Updated") { + continue; + } + if line.starts_with("# ") { + body_content.push(line.to_string()); + continue; + } + if line.starts_with("### ") { + body_content.push(line.to_string()); + continue; + } + + // --- Assume it is a started log --- + let split: Vec<&str> = line.split('|').collect(); + + let item = match split.first() { + Some(val) => val, + None => { + error!("Couldn't split correctly, item 1"); + panic!("Couldn't split correctly, item 1"); + } + } + .replace('#', "") + .trim() + .to_string(); + + if unchecked.contains(&item) { + let timestamp = match split.get(1) { + Some(val) => val, + None => { + error!("Couldn't split correctly, item 2"); + panic!("Couldn't split correctly, item 2"); + } + }; + let start_time = match DateTime::parse_from_rfc2822(timestamp) { + Ok(val) => val.with_timezone(&Central), + Err(e) => { + error!("Error parsing time: '{timestamp}' : {e}"); + panic!("Error parsing time: '{timestamp}' : {e}"); + } + }; + + + let elapsed_time: chrono::Duration = now - start_time; + + body_content.push(format!( + "### {item} | {}h {}m | {}/{}/{}", + elapsed_time.num_hours(), + elapsed_time.num_minutes(), + now.month(), + now.day(), + now.year() + )); + } else { + if line != "" { + body_content.push(line.to_string()); + } + } + checked.retain(|val| val != &item); + } + + checked = checked + .into_iter() + .map(|val| { + format!( + "## {val} | {}", + Utc::now().with_timezone(&Central).to_rfc2822() + ) + }) + .collect(); + + debug!("checked: {:#?}", checked); + debug!("unchecked: {:#?}", unchecked); + + body_content.splice(2..2, checked); + + debug!("{:#?}", body_content); + + Client::new() + .put(format!( + "https://{}/index.php/apps/notes/api/v1/notes/{}", + &cfg.server_url, &cfg.logging_note_id + )) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .basic_auth(&cfg.user, Some(&cfg.pswd)) + .body(serde_json::to_string(&NoteUpdate { + content: body_content.join("\n"), + })?) + .send()?; + + Ok(()) +} + +#[derive(Serialize, Deserialize, Debug)] +struct Note { + id: usize, + etag: String, + readonly: bool, + modified: u64, + title: String, + category: String, + content: String, + favorite: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +struct NoteUpdate { + content: String, +} + +#[derive(Serialize, Deserialize, Default)] +struct Config { + user: String, + pswd: String, + primary_note_id: String, + logging_note_id: String, + server_url: String, +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about=None)] +struct CliArgs { + /// Path to config .toml file + #[arg(short, long)] + config_file: PathBuf, + + #[arg(short, long)] + debug: bool, +}