use chrono::{DateTime, Datelike, Utc}; 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, }