From 432327338f0d94f242be1f55c10f1e12f4135c2a Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Mon, 4 Jul 2022 15:01:17 +0200 Subject: [PATCH 01/13] Get list of all recipes from server --- Cargo.lock | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 ++ src/main.rs | 62 ++++++++++++---- 3 files changed, 264 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c20e145..47bd39d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "anyhow" version = "1.0.58" @@ -67,6 +73,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", +] + +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + [[package]] name = "ci_info" version = "0.10.2" @@ -117,8 +143,10 @@ version = "0.0.0" dependencies = [ "anyhow", "base64", + "chrono", "clap", "directories", + "minicaldav", "reqwest", "rusty-hook", "serde", @@ -144,6 +172,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "darling" version = "0.13.4" @@ -227,6 +264,16 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -329,7 +376,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -553,6 +600,28 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "minicaldav" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb263a7d12c40d5f200dda93b3665b9ae714d4fe64a6467938c92d974a579edb" +dependencies = [ + "base64", + "log", + "ureq", + "url", + "xmltree", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.4" @@ -561,7 +630,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -645,6 +714,25 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0" +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.1" @@ -851,6 +939,33 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "rusty-hook" version = "0.11.2" @@ -888,6 +1003,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.6.1" @@ -970,6 +1095,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "strsim" version = "0.10.0" @@ -1036,6 +1167,17 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1172,6 +1314,30 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" +dependencies = [ + "base64", + "chunked_transfer", + "encoding_rs", + "flate2", + "log", + "once_cell", + "rustls", + "url", + "webpki", + "webpki-roots", +] + [[package]] name = "url" version = "2.2.2" @@ -1211,6 +1377,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1307,6 +1479,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" +dependencies = [ + "webpki", +] + [[package]] name = "widestring" version = "0.5.1" @@ -1395,3 +1586,18 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] diff --git a/Cargo.toml b/Cargo.toml index 27288c4..8b130fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,10 @@ version = "1.0" [dependencies.base64] version = "0.13" +[dependencies.chrono] +version = "0.4" +features = ["serde"] + [dependencies.clap] version = "3.2" features = ["cargo"] @@ -29,6 +33,9 @@ features = ["cargo"] [dependencies.directories] version = "4.0" +[dependencies.minicaldav] +version = "0.2" + [dependencies.reqwest] version = "0.11" features = ["json", "blocking"] diff --git a/src/main.rs b/src/main.rs index 1f81074..01de7b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use serde::Deserialize; + // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,6 +10,7 @@ use { self::config::Config, anyhow::anyhow, base64::write::EncoderWriter as Base64Encoder, + chrono::{DateTime, Utc}, clap::{arg, command, ArgMatches, Command}, reqwest::Url, std::io::Write, @@ -28,9 +31,18 @@ fn parse_args() -> ArgMatches { .subcommand( Command::new("import") .about("Import the given URLs into NextCloud's cookbook") - .arg(server_arg) + .arg(server_arg.clone()) .arg(arg!( ... "One or more URLs each pointing to page with a recipe to import in NextCloud")), ) + .subcommand( + Command::new("schedule") + .about("") + .arg(server_arg.clone()) + .arg(arg!(-d --days "") + .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)) + .required(false) + .default_value("7")) + ) .get_matches() } @@ -53,18 +65,7 @@ async fn main() -> anyhow::Result<()> { })?; } Some(("import", sub_matches)) => { - let server_name = sub_matches - .get_one::("server") - .unwrap_or_else(|| { - let servers = &configuration.credentials.servers; - match servers.len() { - 0 => panic!("No NextCloud server set up yet, use '{} init' first", env!("CARGO_BIN_NAME")), - 1 => servers.iter().next().unwrap().0, - _ => panic!("More than one NextCloud server set up, use the '--server' option to specify which one to use. Known servers: {:#?}", servers.keys().collect::>()), - } - }); - - let api_client = ApiClient::new(&server_name, &configuration)?; + let api_client = get_api_client(&sub_matches, &configuration)?; for url in sub_matches .get_many::("url") .expect("At least one url is required") @@ -80,12 +81,47 @@ async fn main() -> anyhow::Result<()> { println!("{:#?}", response); // TODO } } + Some(("schedule", sub_matches)) => { + let api_client = get_api_client(&sub_matches, &configuration)?; + let recipes = api_client + .client + .get(api_client.base_url.join("apps/cookbook/api/recipes")?) + .send() + .await?; + println!("{:#?}", recipes.json::>().await?); // TODO + } _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), }; Ok(()) } +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Recipe { + #[serde(rename = "recipe_id")] + recipe_id: u32, + name: String, + keywords: String, + date_created: DateTime, + date_modified: DateTime, +} + +fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::Result { + let server_name = sub_matches + .get_one::("server") + .unwrap_or_else(|| { + let servers = &configuration.credentials.servers; + match servers.len() { + 0 => panic!("No NextCloud server set up yet, use '{} init' first", env!("CARGO_BIN_NAME")), + 1 => servers.iter().next().unwrap().0, + _ => panic!("More than one NextCloud server set up, use the '--server' option to specify which one to use. Known servers: {:#?}", servers.keys().collect::>()), + } + }); + + ApiClient::new(&server_name, &configuration) +} + struct ApiClient { pub base_url: Url, pub client: reqwest::Client, From 91a17a213d02214b17e2122e88aae5051238522d Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Mon, 4 Jul 2022 15:04:32 +0200 Subject: [PATCH 02/13] Disable bench tests we don't have --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36a8bbf..9b77a84 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,7 +29,7 @@ stages: - cargo tarpaulin ${CARGO_COMMON_ARGS} --locked --verbose -o Xml - cargo test ${CARGO_COMMON_ARGS} -- -Z unstable-options --format json | tee test-results.json - cargo2junit < test-results.json > junit.xml - - cargo bench ${CARGO_COMMON_ARGS} + # - cargo bench ${CARGO_COMMON_ARGS} # DISABLED UNTIL WE HAVE BENCH TESTS docker:build: stage: build From c9a871f5ec33a7a8e92526defbb589cc6420668b Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Mon, 4 Jul 2022 15:05:12 +0200 Subject: [PATCH 03/13] Tarpaulin doesn't need to be always verbose --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9b77a84..7ba9af6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,7 +26,7 @@ stages: CARGO_COMMON_ARGS: --workspace --no-default-features script: - mkdir -p .git/hooks # for cargo-husky - - cargo tarpaulin ${CARGO_COMMON_ARGS} --locked --verbose -o Xml + - cargo tarpaulin ${CARGO_COMMON_ARGS} --locked -o Xml - cargo test ${CARGO_COMMON_ARGS} -- -Z unstable-options --format json | tee test-results.json - cargo2junit < test-results.json > junit.xml # - cargo bench ${CARGO_COMMON_ARGS} # DISABLED UNTIL WE HAVE BENCH TESTS From 4c76734032a0a375181f60da276d00266da45813 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Wed, 27 Jul 2022 23:54:33 +0200 Subject: [PATCH 04/13] Add some more words to ignore --- .cspell/custom-dictionary-workspace.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.cspell/custom-dictionary-workspace.txt b/.cspell/custom-dictionary-workspace.txt index b7b2a19..85eb9d2 100644 --- a/.cspell/custom-dictionary-workspace.txt +++ b/.cspell/custom-dictionary-workspace.txt @@ -1,6 +1,8 @@ +# Custom Dictionary Words # SPDX-FileCopyrightText: 2022 Matteo Settenvini # SPDX-License-Identifier: CC0-1.0 -# Custom Dictionary Words +cobertura +mkdir montecristosoftware reqwest webbrowser From f2013294418cf55cd04db9fc34ba75754d716bdc Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Thu, 28 Jul 2022 10:28:02 +0200 Subject: [PATCH 05/13] Add temporary CSV parsing import command --- Cargo.lock | 217 ++++++++++++++-------------------------------- Cargo.toml | 10 ++- src/api_client.rs | 50 +++++++++++ src/main.rs | 175 ++++++++++++++++++------------------- src/recipe.rs | 18 ++++ 5 files changed, 224 insertions(+), 246 deletions(-) create mode 100644 src/api_client.rs create mode 100644 src/recipe.rs diff --git a/Cargo.lock b/Cargo.lock index 47bd39d..c1e26dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "anyhow" version = "1.0.58" @@ -43,6 +37,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -87,12 +93,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "chunked_transfer" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" - [[package]] name = "ci_info" version = "0.10.2" @@ -145,10 +145,12 @@ dependencies = [ "base64", "chrono", "clap", + "csv", "directories", - "minicaldav", + "ics", "reqwest", "rusty-hook", + "rustydav", "serde", "serde_json", "tokio", @@ -173,12 +175,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] -name = "crc32fast" -version = "1.3.2" +name = "csv" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ - "cfg-if", + "bstr", + "csv-core", + "itoa 0.4.8", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", ] [[package]] @@ -264,16 +279,6 @@ dependencies = [ "instant", ] -[[package]] -name = "flate2" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "fnv" version = "1.0.7" @@ -421,7 +426,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.2", ] [[package]] @@ -462,7 +467,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 1.0.2", "pin-project-lite", "socket2", "tokio", @@ -484,6 +489,12 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ics" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b891481ef6353e3b97118d4650469e379a39e4373a66908c12f99763182826b1" + [[package]] name = "ident_case" version = "1.0.1" @@ -526,6 +537,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.2" @@ -600,28 +617,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "minicaldav" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb263a7d12c40d5f200dda93b3665b9ae714d4fe64a6467938c92d974a579edb" -dependencies = [ - "base64", - "log", - "ureq", - "url", - "xmltree", -] - -[[package]] -name = "miniz_oxide" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" -dependencies = [ - "adler", -] - [[package]] name = "mio" version = "0.8.4" @@ -893,6 +888,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -939,33 +940,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - -[[package]] -name = "rustls" -version = "0.20.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" -dependencies = [ - "log", - "ring", - "sct", - "webpki", -] - [[package]] name = "rusty-hook" version = "0.11.2" @@ -978,6 +952,15 @@ dependencies = [ "toml", ] +[[package]] +name = "rustydav" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4c86c47126ac8bfc573084610e93f4ca8726f3ae7bf6c64bd60476731b6e42" +dependencies = [ + "reqwest", +] + [[package]] name = "ryu" version = "1.0.10" @@ -1003,16 +986,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" version = "2.6.1" @@ -1062,7 +1035,7 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ - "itoa", + "itoa 1.0.2", "ryu", "serde", ] @@ -1074,7 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.2", "ryu", "serde", ] @@ -1095,12 +1068,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "strsim" version = "0.10.0" @@ -1314,30 +1281,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "ureq" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" -dependencies = [ - "base64", - "chunked_transfer", - "encoding_rs", - "flate2", - "log", - "once_cell", - "rustls", - "url", - "webpki", - "webpki-roots", -] - [[package]] name = "url" version = "2.2.2" @@ -1479,25 +1422,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" -dependencies = [ - "webpki", -] - [[package]] name = "widestring" version = "0.5.1" @@ -1586,18 +1510,3 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - -[[package]] -name = "xmltree" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" -dependencies = [ - "xml-rs", -] diff --git a/Cargo.toml b/Cargo.toml index 8b130fb..8dd4aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ version = "0.13" version = "0.4" features = ["serde"] +[dependencies.csv] +version = "1.1" + [dependencies.clap] version = "3.2" features = ["cargo"] @@ -33,13 +36,16 @@ features = ["cargo"] [dependencies.directories] version = "4.0" -[dependencies.minicaldav] -version = "0.2" +[dependencies.ics] +version = "0.5" [dependencies.reqwest] version = "0.11" features = ["json", "blocking"] +[dependencies.rustydav] +version = "0.1" + [dependencies.serde] version = "1.0" features = ["derive"] diff --git a/src/api_client.rs b/src/api_client.rs new file mode 100644 index 0000000..2a1be9f --- /dev/null +++ b/src/api_client.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use { + crate::config::Config, crate::constants, anyhow::anyhow, + base64::write::EncoderWriter as Base64Encoder, reqwest::Url, std::io::Write, +}; + +pub struct ApiClient { + pub base_url: Url, + pub client: reqwest::Client, +} + +impl ApiClient { + pub fn new(server_name: &str, configuration: &Config) -> anyhow::Result { + let server = configuration + .credentials + .servers + .get(server_name) + .ok_or_else(|| { + anyhow!( + "Unknown server {}. Did you use '{} init' first? Known servers: {:#?}", + server_name, + env!("CARGO_BIN_NAME"), + configuration.credentials.servers.keys().collect::>() + ) + })?; + + use reqwest::header; + let mut default_headers = header::HeaderMap::new(); + let mut auth_header = b"Basic ".to_vec(); + { + let mut encoder = Base64Encoder::new(&mut auth_header, base64::STANDARD); + write!(encoder, "{}:{}", server.login_name, server.password).unwrap(); + } + let mut auth_header = header::HeaderValue::from_bytes(&auth_header)?; + auth_header.set_sensitive(true); + default_headers.insert(header::AUTHORIZATION, auth_header); + + let client = reqwest::Client::builder() + .user_agent(constants::USER_AGENT) + .default_headers(default_headers) + .build()?; + + Ok(ApiClient { + base_url: Url::parse(&server.url)?, + client: client, + }) + } +} diff --git a/src/main.rs b/src/main.rs index 01de7b6..097f3aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,51 +1,20 @@ -use serde::Deserialize; - // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later +mod api_client; mod config; mod constants; +mod recipe; use { - self::config::Config, - anyhow::anyhow, - base64::write::EncoderWriter as Base64Encoder, - chrono::{DateTime, Utc}, + crate::api_client::ApiClient, + crate::config::Config, + crate::recipe::Recipe, clap::{arg, command, ArgMatches, Command}, - reqwest::Url, - std::io::Write, + reqwest::{StatusCode, Url}, + std::path::PathBuf, }; -fn parse_args() -> ArgMatches { - let server_arg = arg!(-s --server "NextCloud server to connect to").required(false); - - command!() - .propagate_version(true) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("init") - .about("Authenticate against the provided NextCloud server") - .arg(arg!( "NextCloud server to connect to")), - ) - .subcommand( - Command::new("import") - .about("Import the given URLs into NextCloud's cookbook") - .arg(server_arg.clone()) - .arg(arg!( ... "One or more URLs each pointing to page with a recipe to import in NextCloud")), - ) - .subcommand( - Command::new("schedule") - .about("") - .arg(server_arg.clone()) - .arg(arg!(-d --days "") - .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)) - .required(false) - .default_value("7")) - ) - .get_matches() -} - #[tokio::main(flavor = "multi_thread")] async fn main() -> anyhow::Result<()> { let args = parse_args(); @@ -79,6 +48,7 @@ async fn main() -> anyhow::Result<()> { .send() .await?; println!("{:#?}", response); // TODO + assert!([StatusCode::OK, StatusCode::CONFLICT].contains(&response.status())); } } Some(("schedule", sub_matches)) => { @@ -90,21 +60,89 @@ async fn main() -> anyhow::Result<()> { .await?; println!("{:#?}", recipes.json::>().await?); // TODO } + Some(("schedule-csv", sub_matches)) => { + let csv_file = sub_matches + .get_one::("csv_file") + .expect(" is a mandatory parameter, it cannot be missing"); + let calendar_name = sub_matches + .get_one::("calendar_name") + .expect(" is a mandatory parameter, it cannot be missing"); + + let mut csv = csv::Reader::from_path(csv_file)?; + + #[derive(serde::Deserialize)] + struct CsvRecord { + day: chrono::naive::NaiveDate, + lunch: String, + dinner: String, + } + + let recipe_urls = csv.deserialize::().fold( + std::collections::HashSet::new(), + |mut set, r| { + if let Ok(r) = r { + set.insert(r.lunch); + set.insert(r.dinner); + } + set + }, + ); + + let api_client = get_api_client(&sub_matches, &configuration)?; + for url in recipe_urls { + let response = api_client + .client + .post(api_client.base_url.join("apps/cookbook/import")?) + .json(&serde_json::json!({ + "url": url, + })) + .send() + .await?; + println!("{:#?}", response); // TODO + assert!([StatusCode::OK, StatusCode::CONFLICT].contains(&response.status())); + } + } _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), }; Ok(()) } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Recipe { - #[serde(rename = "recipe_id")] - recipe_id: u32, - name: String, - keywords: String, - date_created: DateTime, - date_modified: DateTime, +fn parse_args() -> ArgMatches { + let server_arg = arg!(-s --server "NextCloud server to connect to").required(false); + + command!() + .propagate_version(true) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("init") + .about("Authenticate against the provided NextCloud server") + .arg(arg!( "NextCloud server to connect to")), + ) + .subcommand( + Command::new("import") + .about("Import the given URLs into NextCloud's cookbook") + .arg(server_arg.clone()) + .arg(arg!( ... "One or more URLs each pointing to page with a recipe to import in NextCloud")), + ) + .subcommand( + Command::new("schedule") + .about("") + .arg(server_arg.clone()) + .arg(arg!(-d --days "") + .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)) + .required(false) + .default_value("7")) + ) + .subcommand( + Command::new("schedule-csv") + .about("TEMPORARY WIP FUNCTION, UNSTABLE") + .arg(server_arg.clone()) + .arg(arg!( "")) + .arg(arg!( "").value_parser(clap::value_parser!(PathBuf))) + ) + .get_matches() } fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::Result { @@ -121,46 +159,3 @@ fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::R ApiClient::new(&server_name, &configuration) } - -struct ApiClient { - pub base_url: Url, - pub client: reqwest::Client, -} - -impl ApiClient { - pub fn new(server_name: &str, configuration: &Config) -> anyhow::Result { - let server = configuration - .credentials - .servers - .get(server_name) - .ok_or_else(|| { - anyhow!( - "Unknown server {}. Did you use '{} init' first? Known servers: {:#?}", - server_name, - env!("CARGO_BIN_NAME"), - configuration.credentials.servers.keys().collect::>() - ) - })?; - - use reqwest::header; - let mut default_headers = header::HeaderMap::new(); - let mut auth_header = b"Basic ".to_vec(); - { - let mut encoder = Base64Encoder::new(&mut auth_header, base64::STANDARD); - write!(encoder, "{}:{}", server.login_name, server.password).unwrap(); - } - let mut auth_header = header::HeaderValue::from_bytes(&auth_header)?; - auth_header.set_sensitive(true); - default_headers.insert(header::AUTHORIZATION, auth_header); - - let client = reqwest::Client::builder() - .user_agent(constants::USER_AGENT) - .default_headers(default_headers) - .build()?; - - Ok(ApiClient { - base_url: Url::parse(&server.url)?, - client: client, - }) - } -} diff --git a/src/recipe.rs b/src/recipe.rs new file mode 100644 index 0000000..ab56983 --- /dev/null +++ b/src/recipe.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use { + chrono::{DateTime, Utc}, + serde::Deserialize, +}; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Recipe { + #[serde(rename = "recipe_id")] + recipe_id: u32, + name: String, + keywords: String, + date_created: DateTime, + date_modified: DateTime, +} From dce761b0f9b678fc48539ce06808db85f3b7cf02 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Thu, 28 Jul 2022 16:38:12 +0200 Subject: [PATCH 06/13] Create (but not publish) events from recipes --- .gitignore | 1 + .vscode/launch.json | 44 +++++++++ Cargo.lock | 98 +++++++++++++++++-- Cargo.toml | 12 ++- src/api_client.rs | 8 +- src/commands/import.rs | 30 ++++++ src/commands/init.rs | 14 +++ src/commands/mod.rs | 7 ++ src/commands/schedule.rs | 14 +++ src/commands/schedule_csv.rs | 181 +++++++++++++++++++++++++++++++++++ src/constants.rs | 1 + src/main.rs | 139 +++++++++------------------ src/recipe.rs | 130 +++++++++++++++++++++++-- 13 files changed, 560 insertions(+), 119 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/commands/import.rs create mode 100644 src/commands/init.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/schedule.rs create mode 100644 src/commands/schedule_csv.rs diff --git a/.gitignore b/.gitignore index 03a17e2..803c8e0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ # SPDX-License-Identifier: CC0-1.0 /target +/*.csv \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1ae0955 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,44 @@ +{ + // SPDX-FileCopyrightText: 2022 Matteo Settenvini + // SPDX-License-Identifier: CC0-1.0 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'cook'", + "cargo": { + "args": [ + "build", + "--bin=cook", + "--package=cooking-schedule" + ], + "filter": { + "name": "cook", + "kind": "bin" + } + }, + "args": ["schedule-csv", "Cucina", "example-schedule.csv"], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'cook'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=cook", + "--package=cooking-schedule" + ], + "filter": { + "name": "cook", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c1e26dc..5844dba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,12 +147,14 @@ dependencies = [ "clap", "csv", "directories", + "futures", "ics", "reqwest", "rusty-hook", - "rustydav", "serde", "serde_json", + "speedate", + "strum_macros", "tokio", "toml", "webbrowser", @@ -316,6 +318,21 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd087255f739f4f1aeea69f11b72f8080e9c2e7645cd06955dad4a178a49e3" +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.21" @@ -323,6 +340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -331,12 +349,34 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.21" @@ -355,8 +395,11 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -409,6 +452,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -940,6 +989,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustversion" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" + [[package]] name = "rusty-hook" version = "0.11.2" @@ -952,15 +1007,6 @@ dependencies = [ "toml", ] -[[package]] -name = "rustydav" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4c86c47126ac8bfc573084610e93f4ca8726f3ae7bf6c64bd60476731b6e42" -dependencies = [ - "reqwest", -] - [[package]] name = "ryu" version = "1.0.10" @@ -1068,12 +1114,44 @@ dependencies = [ "winapi", ] +[[package]] +name = "speedate" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519ab0d5d5dc6d050a2327ea508d20b460dba1e3a76f1933c56b7c4da2b5c620" +dependencies = [ + "strum", + "strum_macros", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4faebde00e8ff94316c01800f9054fd2ba77d30d9e922541913051d1d978918b" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "1.0.98" diff --git a/Cargo.toml b/Cargo.toml index 8dd4aeb..761f927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ features = ["cargo"] [dependencies.directories] version = "4.0" +[dependencies.futures] +version = "0.3" + [dependencies.ics] version = "0.5" @@ -43,9 +46,6 @@ version = "0.5" version = "0.11" features = ["json", "blocking"] -[dependencies.rustydav] -version = "0.1" - [dependencies.serde] version = "1.0" features = ["derive"] @@ -53,6 +53,12 @@ features = ["derive"] [dependencies.serde_json] version = "1.0" +[dependencies.speedate] +version = "0.6" + +[dependencies.strum_macros] +version = "0.24" + [dependencies.toml] version = "0.5" diff --git a/src/api_client.rs b/src/api_client.rs index 2a1be9f..6acb3e4 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -8,7 +8,8 @@ use { pub struct ApiClient { pub base_url: Url, - pub client: reqwest::Client, + pub username: String, + pub rest: reqwest::Client, } impl ApiClient { @@ -37,14 +38,15 @@ impl ApiClient { auth_header.set_sensitive(true); default_headers.insert(header::AUTHORIZATION, auth_header); - let client = reqwest::Client::builder() + let rest_client = reqwest::Client::builder() .user_agent(constants::USER_AGENT) .default_headers(default_headers) .build()?; Ok(ApiClient { base_url: Url::parse(&server.url)?, - client: client, + username: server.login_name.clone(), + rest: rest_client, }) } } diff --git a/src/commands/import.rs b/src/commands/import.rs new file mode 100644 index 0000000..3ac8331 --- /dev/null +++ b/src/commands/import.rs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use {crate::api_client::ApiClient, anyhow::Result, reqwest::StatusCode}; + +pub async fn with(api_client: &ApiClient, urls: UrlsIter) -> Result<()> +where + UrlsIter: std::iter::Iterator, + UrlsIter::Item: AsRef, +{ + for url in urls { + let response = api_client + .rest + .post(api_client.base_url.join("apps/cookbook/import")?) + .json(&serde_json::json!({ + "url": url.as_ref(), + })) + .send() + .await?; + if ![StatusCode::OK, StatusCode::CONFLICT].contains(&response.status()) { + anyhow::bail!( + "Unable to import recipe {}, received status code {}", + url.as_ref(), + response.status() + ); + } + } + + Ok(()) +} diff --git a/src/commands/init.rs b/src/commands/init.rs new file mode 100644 index 0000000..5c4add4 --- /dev/null +++ b/src/commands/init.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use {crate::config::Config, anyhow::Result, reqwest::Url}; + +pub async fn with(configuration: &mut Config, server: &str) -> Result<()> { + tokio::task::block_in_place(move || -> anyhow::Result<()> { + configuration + .credentials + .add(Url::parse(server)?) + .expect("Unable to authenticate to NextCloud instance"); + Ok(()) + }) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..32611c5 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod import; +pub mod init; +pub mod schedule; +pub mod schedule_csv; diff --git a/src/commands/schedule.rs b/src/commands/schedule.rs new file mode 100644 index 0000000..f92cab2 --- /dev/null +++ b/src/commands/schedule.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use {crate::api_client::ApiClient, crate::recipe, anyhow::Result}; + +pub async fn with(api_client: &ApiClient) -> Result<()> { + let recipes = api_client + .rest + .get(api_client.base_url.join("apps/cookbook/api/recipes")?) + .send() + .await?; + println!("{:#?}", recipes.json::>().await?); + todo!(); +} diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs new file mode 100644 index 0000000..51e7ab9 --- /dev/null +++ b/src/commands/schedule_csv.rs @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use { + crate::api_client::ApiClient, + crate::commands::import, + crate::constants, + crate::recipe, + anyhow::Result, + chrono::naive::{NaiveDate, NaiveDateTime, NaiveTime}, + chrono::{DateTime, Local, TimeZone}, + futures::future::try_join_all, + ics::properties as calprop, + ics::Event, + std::collections::{HashMap, HashSet}, + std::iter::Iterator, + std::path::Path, +}; + +#[derive(serde::Deserialize)] +struct CsvRecord { + day: NaiveDate, + lunch: String, + dinner: String, +} + +#[derive(strum_macros::Display)] +enum Meal { + Lunch, + Dinner, +} + +pub async fn with(api_client: &ApiClient, calendar: &str, csv_file: &Path) -> Result<()> { + let mut csv = csv::Reader::from_path(csv_file)?; + let records = csv.deserialize::().flatten().collect::>(); + + let recipe_urls = urls_from_csv(records.iter())?; + import::with(&api_client, recipe_urls.into_iter()).await?; + let recipes = get_all_recipes(&api_client).await?; + + let events = records + .iter() + .flat_map(|r| { + let lunch = recipes.get(&r.lunch); + let dinner = recipes.get(&r.dinner); + + let events = [ + lunch.map(|recipe| to_event(r.day, Meal::Lunch, recipe)), + dinner.map(|recipe| to_event(r.day, Meal::Dinner, recipe)), + ]; + + events + }) + .flatten(); + + publish_events(&api_client, calendar, events)?; + Ok(()) +} + +fn to_event(date: NaiveDate, meal: Meal, recipe: &recipe::Recipe) -> Event { + // TODO: this is momentarily hardcoded, should be an option + let meal_time = match meal { + Meal::Lunch => NaiveTime::from_hms(12, 00, 00), + Meal::Dinner => NaiveTime::from_hms(19, 00, 00), + }; + + let uid = format!( + "{}-{}@{}.montecristosoftware.eu", + date, + meal, + env!("CARGO_PKG_NAME") + ); + + let end_time = NaiveDateTime::new(date, meal_time); + let start_time = end_time - recipe.total_time(); + + let mut event = Event::new(uid, dt_fmt(&Local::now())); + event.push(calprop::Summary::new(&recipe.name)); + event.push(calprop::Description::new(format!("cookbook@{}", recipe.id))); + event.push(calprop::Location::new(&recipe.url)); + event.push(calprop::DtStart::new(dt_fmt( + &Local.from_local_datetime(&start_time).unwrap(), + ))); + event.push(calprop::DtEnd::new(dt_fmt( + &Local.from_local_datetime(&end_time).unwrap(), + ))); + + let alarm = ics::Alarm::display( + calprop::Trigger::new("-P15M"), + calprop::Description::new(&recipe.name), + ); + event.add_alarm(alarm); + + event +} + +fn urls_from_csv<'a, RecordsIter>(records: RecordsIter) -> Result> +where + RecordsIter: Iterator, +{ + Ok( + records.fold(std::collections::HashSet::new(), |mut set, r| { + if !r.lunch.is_empty() { + set.insert(r.lunch.clone()); + } + if !r.dinner.is_empty() { + set.insert(r.dinner.clone()); + } + set + }), + ) +} + +fn dt_fmt(datetime: &DateTime) -> String { + datetime.format("%Y%m%dT%H%M%SZ").to_string() +} + +async fn get_all_recipes(api_client: &ApiClient) -> Result> { + let metadata = api_client + .rest + .get(api_client.base_url.join("apps/cookbook/api/recipes")?) + .send() + .await? + .json::>() + .await?; + + let recipes = metadata.iter().map(|rm| async { + let response = api_client + .rest + .get( + api_client + .base_url + .join(&format!("apps/cookbook/api/recipes/{id}", id = rm.id)) + .unwrap(), + ) + .send() + .await + .expect(&format!( + "Cannot fetch recipe {} with id {}", + rm.name, rm.id + )); + response.json::().await + }); + + let recipes = try_join_all(recipes).await?; + Ok(HashMap::from_iter( + recipes.into_iter().map(|r| (r.url.clone(), r)), + )) +} + +fn publish_events<'a, EventsIter>( + api_client: &ApiClient, + calendar: &str, + events: EventsIter, +) -> Result<()> +where + EventsIter: Iterator>, +{ + let calendar_url = api_client.base_url.join(&format!( + "remote.php/dav/calendars/{}/{}/", + &api_client.username, + calendar.to_lowercase().as_str().replace(" ", "-") + )); + + let cal = ics::ICalendar::new( + "2.0", + format!( + "-//{}//NONSGML {}//EN", + constants::VENDOR, + env!("CARGO_PKG_NAME") + ), + ); + let cal = events.fold(cal, |mut cal, e| { + cal.add_event(e); + cal + }); + + println!("CALENDAR: \n {:?}", cal); + + Ok(()) +} diff --git a/src/constants.rs b/src/constants.rs index 8fb4a1c..4c99f75 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -2,3 +2,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); +pub const VENDOR: &str = "montecristosoftware.eu"; diff --git a/src/main.rs b/src/main.rs index 097f3aa..3fb9c3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later mod api_client; +mod commands; mod config; mod constants; mod recipe; @@ -9,106 +10,18 @@ mod recipe; use { crate::api_client::ApiClient, crate::config::Config, - crate::recipe::Recipe, + anyhow::Result, clap::{arg, command, ArgMatches, Command}, - reqwest::{StatusCode, Url}, std::path::PathBuf, }; #[tokio::main(flavor = "multi_thread")] -async fn main() -> anyhow::Result<()> { - let args = parse_args(); - let mut configuration = Config::new(); - - match args.subcommand() { - Some(("init", sub_matches)) => { - let server = sub_matches - .get_one::("server") - .expect("Mandatory parameter "); - tokio::task::block_in_place(move || -> anyhow::Result<()> { - configuration - .credentials - .add(Url::parse(server)?) - .expect("Unable to authenticate to NextCloud instance"); - Ok(()) - })?; - } - Some(("import", sub_matches)) => { - let api_client = get_api_client(&sub_matches, &configuration)?; - for url in sub_matches - .get_many::("url") - .expect("At least one url is required") - { - let response = api_client - .client - .post(api_client.base_url.join("apps/cookbook/import")?) - .json(&serde_json::json!({ - "url": url, - })) - .send() - .await?; - println!("{:#?}", response); // TODO - assert!([StatusCode::OK, StatusCode::CONFLICT].contains(&response.status())); - } - } - Some(("schedule", sub_matches)) => { - let api_client = get_api_client(&sub_matches, &configuration)?; - let recipes = api_client - .client - .get(api_client.base_url.join("apps/cookbook/api/recipes")?) - .send() - .await?; - println!("{:#?}", recipes.json::>().await?); // TODO - } - Some(("schedule-csv", sub_matches)) => { - let csv_file = sub_matches - .get_one::("csv_file") - .expect(" is a mandatory parameter, it cannot be missing"); - let calendar_name = sub_matches - .get_one::("calendar_name") - .expect(" is a mandatory parameter, it cannot be missing"); - - let mut csv = csv::Reader::from_path(csv_file)?; - - #[derive(serde::Deserialize)] - struct CsvRecord { - day: chrono::naive::NaiveDate, - lunch: String, - dinner: String, - } - - let recipe_urls = csv.deserialize::().fold( - std::collections::HashSet::new(), - |mut set, r| { - if let Ok(r) = r { - set.insert(r.lunch); - set.insert(r.dinner); - } - set - }, - ); - - let api_client = get_api_client(&sub_matches, &configuration)?; - for url in recipe_urls { - let response = api_client - .client - .post(api_client.base_url.join("apps/cookbook/import")?) - .json(&serde_json::json!({ - "url": url, - })) - .send() - .await?; - println!("{:#?}", response); // TODO - assert!([StatusCode::OK, StatusCode::CONFLICT].contains(&response.status())); - } - } - _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), - }; - - Ok(()) +async fn main() -> Result<()> { + let args = setup_args(); + parse_args(&args).await } -fn parse_args() -> ArgMatches { +fn setup_args() -> ArgMatches { let server_arg = arg!(-s --server "NextCloud server to connect to").required(false); command!() @@ -137,7 +50,7 @@ fn parse_args() -> ArgMatches { ) .subcommand( Command::new("schedule-csv") - .about("TEMPORARY WIP FUNCTION, UNSTABLE") + .about("TEMPORARY WIP FUNCTION USED FOR INTERNAL TESTING") .arg(server_arg.clone()) .arg(arg!( "")) .arg(arg!( "").value_parser(clap::value_parser!(PathBuf))) @@ -145,7 +58,43 @@ fn parse_args() -> ArgMatches { .get_matches() } -fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::Result { +async fn parse_args(args: &ArgMatches) -> Result<()> { + let mut configuration = Config::new(); + + match args.subcommand() { + Some(("init", sub_matches)) => { + let server = sub_matches + .get_one::("server") + .expect("Mandatory parameter "); + commands::init::with(&mut configuration, server).await + } + Some(("import", sub_matches)) => { + let api_client = get_api_client(&sub_matches, &configuration)?; + let urls = sub_matches + .get_many::("url") + .expect("At least one url is required") + .map(|s| s.as_str()); + commands::import::with(&api_client, urls).await + } + Some(("schedule", sub_matches)) => { + let api_client = get_api_client(&sub_matches, &configuration)?; + commands::schedule::with(&api_client).await + } + Some(("schedule-csv", sub_matches)) => { + let api_client = get_api_client(&sub_matches, &configuration)?; + let csv_file = sub_matches + .get_one::("csv_file") + .expect(" is a mandatory parameter, it cannot be missing"); + let calendar_name = sub_matches + .get_one::("calendar_name") + .expect(" is a mandatory parameter, it cannot be missing"); + commands::schedule_csv::with(&api_client, calendar_name.as_str(), &csv_file).await + } + _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), + } +} + +fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> Result { let server_name = sub_matches .get_one::("server") .unwrap_or_else(|| { diff --git a/src/recipe.rs b/src/recipe.rs index ab56983..7dfd1df 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -2,17 +2,131 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use { - chrono::{DateTime, Utc}, - serde::Deserialize, + chrono::Duration, + serde::{Deserialize, Deserializer}, }; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct Recipe { +pub struct Metadata { #[serde(rename = "recipe_id")] - recipe_id: u32, - name: String, - keywords: String, - date_created: DateTime, - date_modified: DateTime, + pub id: u32, + pub name: String, + pub keywords: String, + pub date_created: DateTime, + pub date_modified: DateTime, +} + +/// A recipe according to [schema.org](http://schema.org/Recipe) +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Recipe { + pub id: isize, + pub name: String, + pub description: String, + pub url: String, + pub keywords: String, + + #[serde(rename = "dateCreated")] + pub created: DateTime, + + #[serde(rename = "dateModified")] + pub modified: Option, + + pub image_url: String, + + #[serde(deserialize_with = "deserialize_duration")] + pub prep_time: Duration, + + #[serde(default)] + #[serde(deserialize_with = "deserialize_maybe_duration")] + pub cook_time: Option, + + #[serde(default)] + #[serde(deserialize_with = "deserialize_maybe_duration")] + pub total_time: Option, + + pub image: Option, + pub recipe_yield: isize, + + #[serde(rename = "recipeCategory")] + pub category: Option, + + pub tools: Option>, + #[serde(rename = "recipeIngredient")] + pub ingredients: Vec, + #[serde(rename = "recipeInstructions")] + pub instructions: Vec, + //pub nutrition: Nutrition, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Ingredient(String); + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Tool(String); + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Instruction(String); + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Nutrition { + pub calories: Option, + pub carbohydrates_content: Option, + pub cholesterol_content: Option, + pub fat_content: Option, + pub fiber_content: Option, + pub protein_content: Option, + pub saturated_fat_content: Option, + pub serving_size: Option, + pub sodium_content: Option, + pub sugar_content: Option, + pub trans_fat_content: Option, + pub unsaturated_fat_content: Option, +} + +type DateTime = chrono::DateTime; + +fn deserialize_maybe_duration<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Some(deserialize_duration(deserializer)?)) +} + +fn deserialize_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_str(DurationVisitor) +} + +struct DurationVisitor; + +impl<'de> serde::de::Visitor<'de> for DurationVisitor { + type Value = Duration; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a duration in ISO 8601 format") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + speedate::Duration::parse_str(value) + .map(|dt| Duration::seconds(dt.signed_total_seconds())) + .map_err(|e| E::custom(e.to_string())) + } +} + +impl Recipe { + pub fn total_time(&self) -> Duration { + self.total_time + .unwrap_or_else(|| self.prep_time + self.cook_time.unwrap_or(Duration::zero())) + } } From 8e1547750ef159ca19da6815493ec5dbec47c494 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Thu, 28 Jul 2022 18:32:38 +0200 Subject: [PATCH 07/13] Publish cal events to server --- Cargo.lock | 47 ++++++++++++++++++++++++ Cargo.toml | 4 +++ src/commands/schedule_csv.rs | 70 ++++++++++++++++++++++++++---------- 3 files changed, 102 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5844dba..3a34c7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,7 @@ dependencies = [ "strum_macros", "tokio", "toml", + "uuid", "webbrowser", ] @@ -889,6 +890,12 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -917,6 +924,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -1371,6 +1408,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +dependencies = [ + "getrandom", + "rand", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 761f927..a6e6247 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,5 +66,9 @@ version = "0.5" version = "1" features = ["rt-multi-thread", "net", "macros"] +[dependencies.uuid] +version = "1.1" +features = ["v4", "fast-rng"] + [dependencies.webbrowser] version = "0.7" diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index 51e7ab9..c37a89b 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -6,13 +6,15 @@ use { crate::commands::import, crate::constants, crate::recipe, - anyhow::Result, + anyhow::{bail, Result}, chrono::naive::{NaiveDate, NaiveDateTime, NaiveTime}, chrono::{DateTime, Local, TimeZone}, futures::future::try_join_all, ics::properties as calprop, ics::Event, + reqwest::StatusCode, std::collections::{HashMap, HashSet}, + std::fmt::Write, std::iter::Iterator, std::path::Path, }; @@ -53,11 +55,11 @@ pub async fn with(api_client: &ApiClient, calendar: &str, csv_file: &Path) -> Re }) .flatten(); - publish_events(&api_client, calendar, events)?; + publish_events(&api_client, calendar, events).await?; Ok(()) } -fn to_event(date: NaiveDate, meal: Meal, recipe: &recipe::Recipe) -> Event { +fn to_event(date: NaiveDate, meal: Meal, recipe: &recipe::Recipe) -> (String, Event) { // TODO: this is momentarily hardcoded, should be an option let meal_time = match meal { Meal::Lunch => NaiveTime::from_hms(12, 00, 00), @@ -74,7 +76,7 @@ fn to_event(date: NaiveDate, meal: Meal, recipe: &recipe::Recipe) -> Event { let end_time = NaiveDateTime::new(date, meal_time); let start_time = end_time - recipe.total_time(); - let mut event = Event::new(uid, dt_fmt(&Local::now())); + let mut event = Event::new(uid.clone(), dt_fmt(&Local::now())); event.push(calprop::Summary::new(&recipe.name)); event.push(calprop::Description::new(format!("cookbook@{}", recipe.id))); event.push(calprop::Location::new(&recipe.url)); @@ -91,7 +93,7 @@ fn to_event(date: NaiveDate, meal: Meal, recipe: &recipe::Recipe) -> Event { ); event.add_alarm(alarm); - event + (uid, event) } fn urls_from_csv<'a, RecordsIter>(records: RecordsIter) -> Result> @@ -112,7 +114,7 @@ where } fn dt_fmt(datetime: &DateTime) -> String { - datetime.format("%Y%m%dT%H%M%SZ").to_string() + datetime.format("%Y%m%dT%H%M%S").to_string() } async fn get_all_recipes(api_client: &ApiClient) -> Result> { @@ -148,21 +150,15 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result( +async fn publish_events<'a, EventsIter>( api_client: &ApiClient, calendar: &str, events: EventsIter, ) -> Result<()> where - EventsIter: Iterator>, + EventsIter: Iterator)>, { - let calendar_url = api_client.base_url.join(&format!( - "remote.php/dav/calendars/{}/{}/", - &api_client.username, - calendar.to_lowercase().as_str().replace(" ", "-") - )); - - let cal = ics::ICalendar::new( + let calendar_prototype: ics::ICalendar = ics::ICalendar::new( "2.0", format!( "-//{}//NONSGML {}//EN", @@ -170,12 +166,48 @@ where env!("CARGO_PKG_NAME") ), ); - let cal = events.fold(cal, |mut cal, e| { - cal.add_event(e); - cal + + let dav_base = api_client + .rest + .head(api_client.base_url.join("/.well-known/caldav")?) + .send() + .await?; + + let calendar_url = dav_base.url().join(&format!( + "calendars/{}/{}/", + &api_client.username, + calendar.to_lowercase().as_str().replace(" ", "-") + ))?; + + let calendar_prototype = &calendar_prototype; + let calendar_url = &calendar_url; + let update_requests = events.map(|(event_id, event)| async move { + let mut cal = calendar_prototype.clone(); + cal.add_event(event); + + let url = calendar_url.join(&format!("{}.ics", event_id)).unwrap(); + api_client + .rest + .put(url) + .header("Content-Type", "text/calendar; charset=utf-8") + .body(cal.to_string()) + .send() + .await }); - println!("CALENDAR: \n {:?}", cal); + let responses = try_join_all(update_requests).await?; + let failed_responses = responses.into_iter().filter(|response| { + ![StatusCode::NO_CONTENT, StatusCode::CREATED].contains(&response.status()) + }); + + let mut errors = String::new(); + for r in failed_responses { + write!(errors, "\n{}", r.text().await.unwrap())?; + } + + if !errors.is_empty() { + bail!("Error while updating calendar events: {}", errors); + } Ok(()) } From 0d53be51a2904333ad94690fad7edc08d2fc6ac0 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Fri, 29 Jul 2022 02:19:47 +0200 Subject: [PATCH 08/13] Make events repeat yearly for now --- src/commands/schedule_csv.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index c37a89b..50615d3 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -87,6 +87,9 @@ fn to_event(date: NaiveDate, meal: Meal, recipe: &recipe::Recipe) -> (String, Ev &Local.from_local_datetime(&end_time).unwrap(), ))); + // TODO make configurable yearly repetition, for now this suits my personal uses + event.push(calprop::RRule::new("FREQ=YEARLY")); + let alarm = ics::Alarm::display( calprop::Trigger::new("-P15M"), calprop::Description::new(&recipe.name), From defcb94ee746b61bf8e07afb3b585bd274f870de Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Fri, 29 Jul 2022 02:22:06 +0200 Subject: [PATCH 09/13] Add example CSV import file for testing --- .gitignore | 1 - .vscode/launch.json | 2 +- examples/example-schedule.csv | 366 ++++++++++++++++++++++++++ examples/example-schedule.csv.license | 2 + 4 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 examples/example-schedule.csv create mode 100644 examples/example-schedule.csv.license diff --git a/.gitignore b/.gitignore index 803c8e0..03a17e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ # SPDX-License-Identifier: CC0-1.0 /target -/*.csv \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 1ae0955..2d64fc4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,7 +18,7 @@ "kind": "bin" } }, - "args": ["schedule-csv", "Cucina", "example-schedule.csv"], + "args": ["schedule-csv", "Cucina", "examples/example-schedule.csv"], "cwd": "${workspaceFolder}" }, { diff --git a/examples/example-schedule.csv b/examples/example-schedule.csv new file mode 100644 index 0000000..838e9b9 --- /dev/null +++ b/examples/example-schedule.csv @@ -0,0 +1,366 @@ +"day","lunch","dinner" +2022-08-01,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html","https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" +2022-08-02,, +2022-08-03,, +2022-08-04,, +2022-08-05,, +2022-08-06,, +2022-08-07,, +2022-08-08,, +2022-08-09,, +2022-08-10,, +2022-08-11,, +2022-08-12,, +2022-08-13,, +2022-08-14,, +2022-08-15,, +2022-08-16,, +2022-08-17,, +2022-08-18,, +2022-08-19,, +2022-08-20,, +2022-08-21,, +2022-08-22,, +2022-08-23,, +2022-08-24,, +2022-08-25,, +2022-08-26,, +2022-08-27,, +2022-08-28,, +2022-08-29,, +2022-08-30,, +2022-08-31,, +2022-09-01,, +2022-09-02,, +2022-09-03,, +2022-09-04,, +2022-09-05,, +2022-09-06,, +2022-09-07,, +2022-09-08,, +2022-09-09,, +2022-09-10,, +2022-09-11,, +2022-09-12,, +2022-09-13,, +2022-09-14,, +2022-09-15,, +2022-09-16,, +2022-09-17,, +2022-09-18,, +2022-09-19,, +2022-09-20,, +2022-09-21,, +2022-09-22,, +2022-09-23,, +2022-09-24,, +2022-09-25,, +2022-09-26,, +2022-09-27,, +2022-09-28,, +2022-09-29,, +2022-09-30,, +2022-10-01,, +2022-10-02,, +2022-10-03,, +2022-10-04,, +2022-10-05,, +2022-10-06,, +2022-10-07,, +2022-10-08,, +2022-10-09,, +2022-10-10,, +2022-10-11,, +2022-10-12,, +2022-10-13,, +2022-10-14,, +2022-10-15,, +2022-10-16,, +2022-10-17,, +2022-10-18,, +2022-10-19,, +2022-10-20,, +2022-10-21,, +2022-10-22,, +2022-10-23,, +2022-10-24,, +2022-10-25,, +2022-10-26,, +2022-10-27,, +2022-10-28,, +2022-10-29,, +2022-10-30,, +2022-10-31,, +2022-11-01,, +2022-11-02,, +2022-11-03,, +2022-11-04,, +2022-11-05,, +2022-11-06,, +2022-11-07,, +2022-11-08,, +2022-11-09,, +2022-11-10,, +2022-11-11,, +2022-11-12,, +2022-11-13,, +2022-11-14,, +2022-11-15,, +2022-11-16,, +2022-11-17,, +2022-11-18,, +2022-11-19,, +2022-11-20,, +2022-11-21,, +2022-11-22,, +2022-11-23,, +2022-11-24,, +2022-11-25,, +2022-11-26,, +2022-11-27,, +2022-11-28,, +2022-11-29,, +2022-11-30,, +2022-12-01,, +2022-12-02,, +2022-12-03,, +2022-12-04,, +2022-12-05,, +2022-12-06,, +2022-12-07,, +2022-12-08,, +2022-12-09,, +2022-12-10,, +2022-12-11,, +2022-12-12,, +2022-12-13,, +2022-12-14,, +2022-12-15,, +2022-12-16,, +2022-12-17,, +2022-12-18,, +2022-12-19,, +2022-12-20,, +2022-12-21,, +2022-12-22,, +2022-12-23,, +2022-12-24,, +2022-12-25,, +2022-12-26,, +2022-12-27,, +2022-12-28,, +2022-12-29,, +2022-12-30,, +2022-12-31,, +2023-01-01,, +2023-01-02,, +2023-01-03,, +2023-01-04,, +2023-01-05,, +2023-01-06,, +2023-01-07,, +2023-01-08,, +2023-01-09,, +2023-01-10,, +2023-01-11,, +2023-01-12,, +2023-01-13,, +2023-01-14,, +2023-01-15,, +2023-01-16,, +2023-01-17,, +2023-01-18,, +2023-01-19,, +2023-01-20,, +2023-01-21,, +2023-01-22,, +2023-01-23,, +2023-01-24,, +2023-01-25,, +2023-01-26,, +2023-01-27,, +2023-01-28,, +2023-01-29,, +2023-01-30,, +2023-01-31,, +2023-02-01,, +2023-02-02,, +2023-02-03,, +2023-02-04,, +2023-02-05,, +2023-02-06,, +2023-02-07,, +2023-02-08,, +2023-02-09,, +2023-02-10,, +2023-02-11,, +2023-02-12,, +2023-02-13,, +2023-02-14,, +2023-02-15,, +2023-02-16,, +2023-02-17,, +2023-02-18,, +2023-02-19,, +2023-02-20,, +2023-02-21,, +2023-02-22,, +2023-02-23,, +2023-02-24,, +2023-02-25,, +2023-02-26,, +2023-02-27,, +2023-02-28,, +2023-03-01,, +2023-03-02,, +2023-03-03,, +2023-03-04,, +2023-03-05,, +2023-03-06,, +2023-03-07,, +2023-03-08,, +2023-03-09,, +2023-03-10,, +2023-03-11,, +2023-03-12,, +2023-03-13,, +2023-03-14,, +2023-03-15,, +2023-03-16,, +2023-03-17,, +2023-03-18,, +2023-03-19,, +2023-03-20,, +2023-03-21,, +2023-03-22,, +2023-03-23,, +2023-03-24,, +2023-03-25,, +2023-03-26,, +2023-03-27,, +2023-03-28,, +2023-03-29,, +2023-03-30,, +2023-03-31,, +2023-04-01,, +2023-04-02,, +2023-04-03,, +2023-04-04,, +2023-04-05,, +2023-04-06,, +2023-04-07,, +2023-04-08,, +2023-04-09,, +2023-04-10,, +2023-04-11,, +2023-04-12,, +2023-04-13,, +2023-04-14,, +2023-04-15,, +2023-04-16,, +2023-04-17,, +2023-04-18,, +2023-04-19,, +2023-04-20,, +2023-04-21,, +2023-04-22,, +2023-04-23,, +2023-04-24,, +2023-04-25,, +2023-04-26,, +2023-04-27,, +2023-04-28,, +2023-04-29,, +2023-04-30,, +2023-05-01,, +2023-05-02,, +2023-05-03,, +2023-05-04,, +2023-05-05,, +2023-05-06,, +2023-05-07,, +2023-05-08,, +2023-05-09,, +2023-05-10,, +2023-05-11,, +2023-05-12,, +2023-05-13,, +2023-05-14,, +2023-05-15,, +2023-05-16,, +2023-05-17,, +2023-05-18,, +2023-05-19,, +2023-05-20,, +2023-05-21,, +2023-05-22,, +2023-05-23,, +2023-05-24,, +2023-05-25,, +2023-05-26,, +2023-05-27,, +2023-05-28,, +2023-05-29,, +2023-05-30,, +2023-05-31,, +2023-06-01,, +2023-06-02,, +2023-06-03,, +2023-06-04,, +2023-06-05,, +2023-06-06,, +2023-06-07,, +2023-06-08,, +2023-06-09,, +2023-06-10,, +2023-06-11,, +2023-06-12,, +2023-06-13,, +2023-06-14,, +2023-06-15,, +2023-06-16,, +2023-06-17,, +2023-06-18,, +2023-06-19,, +2023-06-20,, +2023-06-21,, +2023-06-22,, +2023-06-23,, +2023-06-24,, +2023-06-25,, +2023-06-26,, +2023-06-27,, +2023-06-28,, +2023-06-29,, +2023-06-30,, +2023-07-01,, +2023-07-02,, +2023-07-03,, +2023-07-04,, +2023-07-05,, +2023-07-06,, +2023-07-07,, +2023-07-08,, +2023-07-09,, +2023-07-10,, +2023-07-11,, +2023-07-12,, +2023-07-13,, +2023-07-14,, +2023-07-15,, +2023-07-16,, +2023-07-17,, +2023-07-18,, +2023-07-19,, +2023-07-20,, +2023-07-21,, +2023-07-22,, +2023-07-23,, +2023-07-24,, +2023-07-25,, +2023-07-26,, +2023-07-27,, +2023-07-28,, +2023-07-29,, +2023-07-30,, +2023-07-31,, diff --git a/examples/example-schedule.csv.license b/examples/example-schedule.csv.license new file mode 100644 index 0000000..da07410 --- /dev/null +++ b/examples/example-schedule.csv.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2022 Matteo Settenvini +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file From 32ccc1ed72e4687292533f4af0654b6cac03a7f7 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Fri, 29 Jul 2022 11:47:38 +0200 Subject: [PATCH 10/13] Partially refactor Event to change ical impl more freely --- .gitignore | 1 + Cargo.lock | 155 ++++++++ Cargo.toml | 3 + examples/example-schedule.csv | 730 +++++++++++++++++----------------- src/commands/schedule_csv.rs | 73 +--- src/event.rs | 80 ++++ src/main.rs | 1 + 7 files changed, 616 insertions(+), 427 deletions(-) create mode 100644 src/event.rs diff --git a/.gitignore b/.gitignore index 03a17e2..7ed0de4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2022 Matteo Settenvini # SPDX-License-Identifier: CC0-1.0 +.~*# /target diff --git a/Cargo.lock b/Cargo.lock index 3a34c7b..2a83d89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "anyhow" version = "1.0.58" @@ -93,6 +99,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + [[package]] name = "ci_info" version = "0.10.2" @@ -149,6 +161,7 @@ dependencies = [ "directories", "futures", "ics", + "minicaldav", "reqwest", "rusty-hook", "serde", @@ -177,6 +190,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "csv" version = "1.1.6" @@ -282,6 +304,16 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -667,6 +699,28 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "minicaldav" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb263a7d12c40d5f200dda93b3665b9ae714d4fe64a6467938c92d974a579edb" +dependencies = [ + "base64", + "log", + "ureq", + "url", + "xmltree", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.4" @@ -1026,6 +1080,33 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "rustversion" version = "1.0.8" @@ -1069,6 +1150,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.6.1" @@ -1161,6 +1252,12 @@ dependencies = [ "strum_macros", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "strsim" version = "0.10.0" @@ -1396,6 +1493,30 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" +dependencies = [ + "base64", + "chunked_transfer", + "encoding_rs", + "flate2", + "log", + "once_cell", + "rustls", + "url", + "webpki", + "webpki-roots", +] + [[package]] name = "url" version = "2.2.2" @@ -1547,6 +1668,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +dependencies = [ + "webpki", +] + [[package]] name = "widestring" version = "0.5.1" @@ -1635,3 +1775,18 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] diff --git a/Cargo.toml b/Cargo.toml index a6e6247..9ac7e8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,9 @@ version = "0.3" [dependencies.ics] version = "0.5" +[dependencies.minicaldav] +version = "0.2" + [dependencies.reqwest] version = "0.11" features = ["json", "blocking"] diff --git a/examples/example-schedule.csv b/examples/example-schedule.csv index 838e9b9..72261c7 100644 --- a/examples/example-schedule.csv +++ b/examples/example-schedule.csv @@ -1,366 +1,366 @@ "day","lunch","dinner" -2022-08-01,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html","https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" -2022-08-02,, -2022-08-03,, -2022-08-04,, -2022-08-05,, -2022-08-06,, -2022-08-07,, -2022-08-08,, -2022-08-09,, -2022-08-10,, -2022-08-11,, -2022-08-12,, -2022-08-13,, -2022-08-14,, -2022-08-15,, -2022-08-16,, -2022-08-17,, -2022-08-18,, -2022-08-19,, -2022-08-20,, -2022-08-21,, -2022-08-22,, -2022-08-23,, -2022-08-24,, -2022-08-25,, -2022-08-26,, -2022-08-27,, -2022-08-28,, -2022-08-29,, -2022-08-30,, -2022-08-31,, -2022-09-01,, -2022-09-02,, -2022-09-03,, -2022-09-04,, -2022-09-05,, -2022-09-06,, -2022-09-07,, -2022-09-08,, -2022-09-09,, -2022-09-10,, -2022-09-11,, -2022-09-12,, -2022-09-13,, -2022-09-14,, -2022-09-15,, -2022-09-16,, -2022-09-17,, -2022-09-18,, -2022-09-19,, -2022-09-20,, -2022-09-21,, -2022-09-22,, -2022-09-23,, -2022-09-24,, -2022-09-25,, -2022-09-26,, -2022-09-27,, -2022-09-28,, -2022-09-29,, -2022-09-30,, -2022-10-01,, -2022-10-02,, -2022-10-03,, -2022-10-04,, -2022-10-05,, -2022-10-06,, -2022-10-07,, -2022-10-08,, -2022-10-09,, -2022-10-10,, -2022-10-11,, -2022-10-12,, -2022-10-13,, -2022-10-14,, -2022-10-15,, -2022-10-16,, -2022-10-17,, -2022-10-18,, -2022-10-19,, -2022-10-20,, -2022-10-21,, -2022-10-22,, -2022-10-23,, -2022-10-24,, -2022-10-25,, -2022-10-26,, -2022-10-27,, -2022-10-28,, -2022-10-29,, -2022-10-30,, -2022-10-31,, -2022-11-01,, -2022-11-02,, -2022-11-03,, -2022-11-04,, -2022-11-05,, -2022-11-06,, -2022-11-07,, -2022-11-08,, -2022-11-09,, -2022-11-10,, -2022-11-11,, -2022-11-12,, -2022-11-13,, -2022-11-14,, -2022-11-15,, -2022-11-16,, -2022-11-17,, -2022-11-18,, -2022-11-19,, -2022-11-20,, -2022-11-21,, -2022-11-22,, -2022-11-23,, -2022-11-24,, -2022-11-25,, -2022-11-26,, -2022-11-27,, -2022-11-28,, -2022-11-29,, -2022-11-30,, -2022-12-01,, -2022-12-02,, -2022-12-03,, -2022-12-04,, -2022-12-05,, -2022-12-06,, -2022-12-07,, -2022-12-08,, -2022-12-09,, -2022-12-10,, -2022-12-11,, -2022-12-12,, -2022-12-13,, -2022-12-14,, -2022-12-15,, -2022-12-16,, -2022-12-17,, -2022-12-18,, -2022-12-19,, -2022-12-20,, -2022-12-21,, -2022-12-22,, -2022-12-23,, -2022-12-24,, -2022-12-25,, -2022-12-26,, -2022-12-27,, -2022-12-28,, -2022-12-29,, -2022-12-30,, -2022-12-31,, -2023-01-01,, -2023-01-02,, -2023-01-03,, -2023-01-04,, -2023-01-05,, -2023-01-06,, -2023-01-07,, -2023-01-08,, -2023-01-09,, -2023-01-10,, -2023-01-11,, -2023-01-12,, -2023-01-13,, -2023-01-14,, -2023-01-15,, -2023-01-16,, -2023-01-17,, -2023-01-18,, -2023-01-19,, -2023-01-20,, -2023-01-21,, -2023-01-22,, -2023-01-23,, -2023-01-24,, -2023-01-25,, -2023-01-26,, -2023-01-27,, -2023-01-28,, -2023-01-29,, -2023-01-30,, -2023-01-31,, -2023-02-01,, -2023-02-02,, -2023-02-03,, -2023-02-04,, -2023-02-05,, -2023-02-06,, -2023-02-07,, -2023-02-08,, -2023-02-09,, -2023-02-10,, -2023-02-11,, -2023-02-12,, -2023-02-13,, -2023-02-14,, -2023-02-15,, -2023-02-16,, -2023-02-17,, -2023-02-18,, -2023-02-19,, -2023-02-20,, -2023-02-21,, -2023-02-22,, -2023-02-23,, -2023-02-24,, -2023-02-25,, -2023-02-26,, -2023-02-27,, -2023-02-28,, -2023-03-01,, -2023-03-02,, -2023-03-03,, -2023-03-04,, -2023-03-05,, -2023-03-06,, -2023-03-07,, -2023-03-08,, -2023-03-09,, -2023-03-10,, -2023-03-11,, -2023-03-12,, -2023-03-13,, -2023-03-14,, -2023-03-15,, -2023-03-16,, -2023-03-17,, -2023-03-18,, -2023-03-19,, -2023-03-20,, -2023-03-21,, -2023-03-22,, -2023-03-23,, -2023-03-24,, -2023-03-25,, -2023-03-26,, -2023-03-27,, -2023-03-28,, -2023-03-29,, -2023-03-30,, -2023-03-31,, -2023-04-01,, -2023-04-02,, -2023-04-03,, -2023-04-04,, -2023-04-05,, -2023-04-06,, -2023-04-07,, -2023-04-08,, -2023-04-09,, -2023-04-10,, -2023-04-11,, -2023-04-12,, -2023-04-13,, -2023-04-14,, -2023-04-15,, -2023-04-16,, -2023-04-17,, -2023-04-18,, -2023-04-19,, -2023-04-20,, -2023-04-21,, -2023-04-22,, -2023-04-23,, -2023-04-24,, -2023-04-25,, -2023-04-26,, -2023-04-27,, -2023-04-28,, -2023-04-29,, -2023-04-30,, -2023-05-01,, -2023-05-02,, -2023-05-03,, -2023-05-04,, -2023-05-05,, -2023-05-06,, -2023-05-07,, -2023-05-08,, -2023-05-09,, -2023-05-10,, -2023-05-11,, -2023-05-12,, -2023-05-13,, -2023-05-14,, -2023-05-15,, -2023-05-16,, -2023-05-17,, -2023-05-18,, -2023-05-19,, -2023-05-20,, -2023-05-21,, -2023-05-22,, -2023-05-23,, -2023-05-24,, -2023-05-25,, -2023-05-26,, -2023-05-27,, -2023-05-28,, -2023-05-29,, -2023-05-30,, -2023-05-31,, -2023-06-01,, -2023-06-02,, -2023-06-03,, -2023-06-04,, -2023-06-05,, -2023-06-06,, -2023-06-07,, -2023-06-08,, -2023-06-09,, -2023-06-10,, -2023-06-11,, -2023-06-12,, -2023-06-13,, -2023-06-14,, -2023-06-15,, -2023-06-16,, -2023-06-17,, -2023-06-18,, -2023-06-19,, -2023-06-20,, -2023-06-21,, -2023-06-22,, -2023-06-23,, -2023-06-24,, -2023-06-25,, -2023-06-26,, -2023-06-27,, -2023-06-28,, -2023-06-29,, -2023-06-30,, -2023-07-01,, -2023-07-02,, -2023-07-03,, -2023-07-04,, -2023-07-05,, -2023-07-06,, -2023-07-07,, -2023-07-08,, -2023-07-09,, -2023-07-10,, -2023-07-11,, -2023-07-12,, -2023-07-13,, -2023-07-14,, -2023-07-15,, -2023-07-16,, -2023-07-17,, -2023-07-18,, -2023-07-19,, -2023-07-20,, -2023-07-21,, -2023-07-22,, -2023-07-23,, -2023-07-24,, -2023-07-25,, -2023-07-26,, -2023-07-27,, -2023-07-28,, -2023-07-29,, -2023-07-30,, -2023-07-31,, +"2022-08-01","https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html","https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" +"2022-08-02",, +"2022-08-03",, +"2022-08-04",, +"2022-08-05",, +"2022-08-06",, +"2022-08-07",, +"2022-08-08",, +"2022-08-09",, +"2022-08-10",, +"2022-08-11",, +"2022-08-12",, +"2022-08-13",, +"2022-08-14",, +"2022-08-15",, +"2022-08-16",, +"2022-08-17",, +"2022-08-18",, +"2022-08-19",, +"2022-08-20",, +"2022-08-21",, +"2022-08-22",, +"2022-08-23",, +"2022-08-24",, +"2022-08-25",, +"2022-08-26",, +"2022-08-27",, +"2022-08-28",, +"2022-08-29",, +"2022-08-30",, +"2022-08-31",, +"2022-09-01",, +"2022-09-02",, +"2022-09-03",, +"2022-09-04",, +"2022-09-05",, +"2022-09-06",, +"2022-09-07",, +"2022-09-08",, +"2022-09-09",, +"2022-09-10",, +"2022-09-11",, +"2022-09-12",, +"2022-09-13",, +"2022-09-14",, +"2022-09-15",, +"2022-09-16",, +"2022-09-17",, +"2022-09-18",, +"2022-09-19",, +"2022-09-20",, +"2022-09-21",, +"2022-09-22",, +"2022-09-23",, +"2022-09-24",, +"2022-09-25",, +"2022-09-26",, +"2022-09-27",, +"2022-09-28",, +"2022-09-29",, +"2022-09-30",, +"2022-10-01",, +"2022-10-02",, +"2022-10-03",, +"2022-10-04",, +"2022-10-05",, +"2022-10-06",, +"2022-10-07",, +"2022-10-08",, +"2022-10-09",, +"2022-10-10",, +"2022-10-11",, +"2022-10-12",, +"2022-10-13",, +"2022-10-14",, +"2022-10-15",, +"2022-10-16",, +"2022-10-17",, +"2022-10-18",, +"2022-10-19",, +"2022-10-20",, +"2022-10-21",, +"2022-10-22",, +"2022-10-23",, +"2022-10-24",, +"2022-10-25",, +"2022-10-26",, +"2022-10-27",, +"2022-10-28",, +"2022-10-29",, +"2022-10-30",, +"2022-10-31",, +"2022-11-01",, +"2022-11-02",, +"2022-11-03",, +"2022-11-04",, +"2022-11-05",, +"2022-11-06",, +"2022-11-07",, +"2022-11-08",, +"2022-11-09",, +"2022-11-10",, +"2022-11-11",, +"2022-11-12",, +"2022-11-13",, +"2022-11-14",, +"2022-11-15",, +"2022-11-16",, +"2022-11-17",, +"2022-11-18",, +"2022-11-19",, +"2022-11-20",, +"2022-11-21",, +"2022-11-22",, +"2022-11-23",, +"2022-11-24",, +"2022-11-25",, +"2022-11-26",, +"2022-11-27",, +"2022-11-28",, +"2022-11-29",, +"2022-11-30",, +"2022-12-01",, +"2022-12-02",, +"2022-12-03",, +"2022-12-04",, +"2022-12-05",, +"2022-12-06",, +"2022-12-07",, +"2022-12-08",, +"2022-12-09",, +"2022-12-10",, +"2022-12-11",, +"2022-12-12",, +"2022-12-13",, +"2022-12-14",, +"2022-12-15",, +"2022-12-16",, +"2022-12-17",, +"2022-12-18",, +"2022-12-19",, +"2022-12-20",, +"2022-12-21",, +"2022-12-22",, +"2022-12-23",, +"2022-12-24",, +"2022-12-25",, +"2022-12-26",, +"2022-12-27",, +"2022-12-28",, +"2022-12-29",, +"2022-12-30",, +"2022-12-31",, +"2023-01-01",, +"2023-01-02",, +"2023-01-03",, +"2023-01-04",, +"2023-01-05",, +"2023-01-06",, +"2023-01-07",, +"2023-01-08",, +"2023-01-09",, +"2023-01-10",, +"2023-01-11",, +"2023-01-12",, +"2023-01-13",, +"2023-01-14",, +"2023-01-15",, +"2023-01-16",, +"2023-01-17",, +"2023-01-18",, +"2023-01-19",, +"2023-01-20",, +"2023-01-21",, +"2023-01-22",, +"2023-01-23",, +"2023-01-24",, +"2023-01-25",, +"2023-01-26",, +"2023-01-27",, +"2023-01-28",, +"2023-01-29",, +"2023-01-30",, +"2023-01-31",, +"2023-02-01",, +"2023-02-02",, +"2023-02-03",, +"2023-02-04",, +"2023-02-05",, +"2023-02-06",, +"2023-02-07",, +"2023-02-08",, +"2023-02-09",, +"2023-02-10",, +"2023-02-11",, +"2023-02-12",, +"2023-02-13",, +"2023-02-14",, +"2023-02-15",, +"2023-02-16",, +"2023-02-17",, +"2023-02-18",, +"2023-02-19",, +"2023-02-20",, +"2023-02-21",, +"2023-02-22",, +"2023-02-23",, +"2023-02-24",, +"2023-02-25",, +"2023-02-26",, +"2023-02-27",, +"2023-02-28",, +"2023-03-01",, +"2023-03-02",, +"2023-03-03",, +"2023-03-04",, +"2023-03-05",, +"2023-03-06",, +"2023-03-07",, +"2023-03-08",, +"2023-03-09",, +"2023-03-10",, +"2023-03-11",, +"2023-03-12",, +"2023-03-13",, +"2023-03-14",, +"2023-03-15",, +"2023-03-16",, +"2023-03-17",, +"2023-03-18",, +"2023-03-19",, +"2023-03-20",, +"2023-03-21",, +"2023-03-22",, +"2023-03-23",, +"2023-03-24",, +"2023-03-25",, +"2023-03-26",, +"2023-03-27",, +"2023-03-28",, +"2023-03-29",, +"2023-03-30",, +"2023-03-31",, +"2023-04-01",, +"2023-04-02",, +"2023-04-03",, +"2023-04-04",, +"2023-04-05",, +"2023-04-06",, +"2023-04-07",, +"2023-04-08",, +"2023-04-09",, +"2023-04-10",, +"2023-04-11",, +"2023-04-12",, +"2023-04-13",, +"2023-04-14",, +"2023-04-15",, +"2023-04-16",, +"2023-04-17",, +"2023-04-18",, +"2023-04-19",, +"2023-04-20",, +"2023-04-21",, +"2023-04-22",, +"2023-04-23",, +"2023-04-24",, +"2023-04-25",, +"2023-04-26",, +"2023-04-27",, +"2023-04-28",, +"2023-04-29",, +"2023-04-30",, +"2023-05-01",, +"2023-05-02",, +"2023-05-03",, +"2023-05-04",, +"2023-05-05",, +"2023-05-06",, +"2023-05-07",, +"2023-05-08",, +"2023-05-09",, +"2023-05-10",, +"2023-05-11",, +"2023-05-12",, +"2023-05-13",, +"2023-05-14",, +"2023-05-15",, +"2023-05-16",, +"2023-05-17",, +"2023-05-18",, +"2023-05-19",, +"2023-05-20",, +"2023-05-21",, +"2023-05-22",, +"2023-05-23",, +"2023-05-24",, +"2023-05-25",, +"2023-05-26",, +"2023-05-27",, +"2023-05-28",, +"2023-05-29",, +"2023-05-30",, +"2023-05-31",, +"2023-06-01",, +"2023-06-02",, +"2023-06-03",, +"2023-06-04",, +"2023-06-05",, +"2023-06-06",, +"2023-06-07",, +"2023-06-08",, +"2023-06-09",, +"2023-06-10",, +"2023-06-11",, +"2023-06-12",, +"2023-06-13",, +"2023-06-14",, +"2023-06-15",, +"2023-06-16",, +"2023-06-17",, +"2023-06-18",, +"2023-06-19",, +"2023-06-20",, +"2023-06-21",, +"2023-06-22",, +"2023-06-23",, +"2023-06-24",, +"2023-06-25",, +"2023-06-26",, +"2023-06-27",, +"2023-06-28",, +"2023-06-29",, +"2023-06-30",, +"2023-07-01",, +"2023-07-02",, +"2023-07-03",, +"2023-07-04",, +"2023-07-05",, +"2023-07-06",, +"2023-07-07",, +"2023-07-08",, +"2023-07-09",, +"2023-07-10",, +"2023-07-11",, +"2023-07-12",, +"2023-07-13",, +"2023-07-14",, +"2023-07-15",, +"2023-07-16",, +"2023-07-17",, +"2023-07-18",, +"2023-07-19",, +"2023-07-20",, +"2023-07-21",, +"2023-07-22",, +"2023-07-23",, +"2023-07-24",, +"2023-07-25",, +"2023-07-26",, +"2023-07-27",, +"2023-07-28",, +"2023-07-29",, +"2023-07-30",, +"2023-07-31",, \ No newline at end of file diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index 50615d3..4fa0c44 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -5,18 +5,17 @@ use { crate::api_client::ApiClient, crate::commands::import, crate::constants, + crate::event::{Event, Meal}, crate::recipe, anyhow::{bail, Result}, - chrono::naive::{NaiveDate, NaiveDateTime, NaiveTime}, - chrono::{DateTime, Local, TimeZone}, + chrono::naive::NaiveDate, futures::future::try_join_all, - ics::properties as calprop, - ics::Event, reqwest::StatusCode, std::collections::{HashMap, HashSet}, std::fmt::Write, std::iter::Iterator, std::path::Path, + std::rc::Rc, }; #[derive(serde::Deserialize)] @@ -26,12 +25,6 @@ struct CsvRecord { dinner: String, } -#[derive(strum_macros::Display)] -enum Meal { - Lunch, - Dinner, -} - pub async fn with(api_client: &ApiClient, calendar: &str, csv_file: &Path) -> Result<()> { let mut csv = csv::Reader::from_path(csv_file)?; let records = csv.deserialize::().flatten().collect::>(); @@ -47,8 +40,8 @@ pub async fn with(api_client: &ApiClient, calendar: &str, csv_file: &Path) -> Re let dinner = recipes.get(&r.dinner); let events = [ - lunch.map(|recipe| to_event(r.day, Meal::Lunch, recipe)), - dinner.map(|recipe| to_event(r.day, Meal::Dinner, recipe)), + lunch.map(|recipe| Event::new(r.day, Meal::Lunch, recipe.clone())), + dinner.map(|recipe| Event::new(r.day, Meal::Dinner, recipe.clone())), ]; events @@ -59,46 +52,6 @@ pub async fn with(api_client: &ApiClient, calendar: &str, csv_file: &Path) -> Re Ok(()) } -fn to_event(date: NaiveDate, meal: Meal, recipe: &recipe::Recipe) -> (String, Event) { - // TODO: this is momentarily hardcoded, should be an option - let meal_time = match meal { - Meal::Lunch => NaiveTime::from_hms(12, 00, 00), - Meal::Dinner => NaiveTime::from_hms(19, 00, 00), - }; - - let uid = format!( - "{}-{}@{}.montecristosoftware.eu", - date, - meal, - env!("CARGO_PKG_NAME") - ); - - let end_time = NaiveDateTime::new(date, meal_time); - let start_time = end_time - recipe.total_time(); - - let mut event = Event::new(uid.clone(), dt_fmt(&Local::now())); - event.push(calprop::Summary::new(&recipe.name)); - event.push(calprop::Description::new(format!("cookbook@{}", recipe.id))); - event.push(calprop::Location::new(&recipe.url)); - event.push(calprop::DtStart::new(dt_fmt( - &Local.from_local_datetime(&start_time).unwrap(), - ))); - event.push(calprop::DtEnd::new(dt_fmt( - &Local.from_local_datetime(&end_time).unwrap(), - ))); - - // TODO make configurable yearly repetition, for now this suits my personal uses - event.push(calprop::RRule::new("FREQ=YEARLY")); - - let alarm = ics::Alarm::display( - calprop::Trigger::new("-P15M"), - calprop::Description::new(&recipe.name), - ); - event.add_alarm(alarm); - - (uid, event) -} - fn urls_from_csv<'a, RecordsIter>(records: RecordsIter) -> Result> where RecordsIter: Iterator, @@ -116,11 +69,7 @@ where ) } -fn dt_fmt(datetime: &DateTime) -> String { - datetime.format("%Y%m%dT%H%M%S").to_string() -} - -async fn get_all_recipes(api_client: &ApiClient) -> Result> { +async fn get_all_recipes(api_client: &ApiClient) -> Result>> { let metadata = api_client .rest .get(api_client.base_url.join("apps/cookbook/api/recipes")?) @@ -144,7 +93,7 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result().await + response.json::().await.map(|r| Rc::new(r)) }); let recipes = try_join_all(recipes).await?; @@ -159,7 +108,7 @@ async fn publish_events<'a, EventsIter>( events: EventsIter, ) -> Result<()> where - EventsIter: Iterator)>, + EventsIter: Iterator, { let calendar_prototype: ics::ICalendar = ics::ICalendar::new( "2.0", @@ -184,11 +133,11 @@ where let calendar_prototype = &calendar_prototype; let calendar_url = &calendar_url; - let update_requests = events.map(|(event_id, event)| async move { + let update_requests = events.map(|ev| async move { + let url = calendar_url.join(&format!("{}.ics", ev.uid)).unwrap(); let mut cal = calendar_prototype.clone(); - cal.add_event(event); + cal.add_event(ev.into()); - let url = calendar_url.join(&format!("{}.ics", event_id)).unwrap(); api_client .rest .put(url) diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..e79a379 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use { + crate::recipe::Recipe, + chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}, + ics::properties as calprop, + ics::Event as CalEvent, + std::rc::Rc, +}; + +pub struct Event { + pub uid: String, + pub ends_at: NaiveDateTime, + pub recipe: Rc, +} + +#[derive(strum_macros::Display)] +pub enum Meal { + Lunch, + Dinner, +} + +impl Event { + pub fn new(date: NaiveDate, meal: Meal, recipe: Rc) -> Self { + let uid = format!( + "{}-{}@{}.montecristosoftware.eu", + date, + meal, + env!("CARGO_PKG_NAME") + ); + + let meal_time = match meal { + Meal::Lunch => NaiveTime::from_hms(12, 00, 00), + Meal::Dinner => NaiveTime::from_hms(19, 00, 00), + }; + + let ends_at = NaiveDateTime::new(date, meal_time); + + Event { + uid, + ends_at, + recipe, + } + } +} + +impl<'a> From for CalEvent<'a> { + fn from(ev: Event) -> Self { + let start_time = ev.ends_at - ev.recipe.total_time(); + + let mut event = ics::Event::new(ev.uid.clone(), dt_fmt(&Local::now())); + event.push(calprop::Summary::new(ev.recipe.name.clone())); + event.push(calprop::Description::new(format!( + "cookbook@{}", + ev.recipe.id + ))); + event.push(calprop::Location::new(ev.recipe.url.clone())); + event.push(calprop::DtStart::new(dt_fmt( + &Local.from_local_datetime(&start_time).unwrap(), + ))); + event.push(calprop::DtEnd::new(dt_fmt( + &Local.from_local_datetime(&ev.ends_at).unwrap(), + ))); + + // TODO make configurable yearly repetition, for now this suits my personal uses + event.push(calprop::RRule::new("FREQ=YEARLY")); + + let alarm = ics::Alarm::display( + calprop::Trigger::new("-P15M"), + calprop::Description::new(ev.recipe.name.clone()), + ); + event.add_alarm(alarm); + event + } +} + +fn dt_fmt(datetime: &DateTime) -> String { + datetime.format("%Y%m%dT%H%M%S").to_string() +} diff --git a/src/main.rs b/src/main.rs index 3fb9c3d..69aa993 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod api_client; mod commands; mod config; mod constants; +mod event; mod recipe; use { From 096db12c7460dccb404db98960a55eb1cfa33f6a Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Fri, 29 Jul 2022 12:32:25 +0200 Subject: [PATCH 11/13] =?UTF-8?q?Ensure=20DTSTAMP=20is=20in=20UTC,=20as=20?= =?UTF-8?q?per=20RFC=205545=20=C2=A7=203.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/example-schedule.csv | 732 +++++++++++++++++----------------- src/event.rs | 15 +- 2 files changed, 376 insertions(+), 371 deletions(-) diff --git a/examples/example-schedule.csv b/examples/example-schedule.csv index 72261c7..e710405 100644 --- a/examples/example-schedule.csv +++ b/examples/example-schedule.csv @@ -1,366 +1,366 @@ -"day","lunch","dinner" -"2022-08-01","https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html","https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" -"2022-08-02",, -"2022-08-03",, -"2022-08-04",, -"2022-08-05",, -"2022-08-06",, -"2022-08-07",, -"2022-08-08",, -"2022-08-09",, -"2022-08-10",, -"2022-08-11",, -"2022-08-12",, -"2022-08-13",, -"2022-08-14",, -"2022-08-15",, -"2022-08-16",, -"2022-08-17",, -"2022-08-18",, -"2022-08-19",, -"2022-08-20",, -"2022-08-21",, -"2022-08-22",, -"2022-08-23",, -"2022-08-24",, -"2022-08-25",, -"2022-08-26",, -"2022-08-27",, -"2022-08-28",, -"2022-08-29",, -"2022-08-30",, -"2022-08-31",, -"2022-09-01",, -"2022-09-02",, -"2022-09-03",, -"2022-09-04",, -"2022-09-05",, -"2022-09-06",, -"2022-09-07",, -"2022-09-08",, -"2022-09-09",, -"2022-09-10",, -"2022-09-11",, -"2022-09-12",, -"2022-09-13",, -"2022-09-14",, -"2022-09-15",, -"2022-09-16",, -"2022-09-17",, -"2022-09-18",, -"2022-09-19",, -"2022-09-20",, -"2022-09-21",, -"2022-09-22",, -"2022-09-23",, -"2022-09-24",, -"2022-09-25",, -"2022-09-26",, -"2022-09-27",, -"2022-09-28",, -"2022-09-29",, -"2022-09-30",, -"2022-10-01",, -"2022-10-02",, -"2022-10-03",, -"2022-10-04",, -"2022-10-05",, -"2022-10-06",, -"2022-10-07",, -"2022-10-08",, -"2022-10-09",, -"2022-10-10",, -"2022-10-11",, -"2022-10-12",, -"2022-10-13",, -"2022-10-14",, -"2022-10-15",, -"2022-10-16",, -"2022-10-17",, -"2022-10-18",, -"2022-10-19",, -"2022-10-20",, -"2022-10-21",, -"2022-10-22",, -"2022-10-23",, -"2022-10-24",, -"2022-10-25",, -"2022-10-26",, -"2022-10-27",, -"2022-10-28",, -"2022-10-29",, -"2022-10-30",, -"2022-10-31",, -"2022-11-01",, -"2022-11-02",, -"2022-11-03",, -"2022-11-04",, -"2022-11-05",, -"2022-11-06",, -"2022-11-07",, -"2022-11-08",, -"2022-11-09",, -"2022-11-10",, -"2022-11-11",, -"2022-11-12",, -"2022-11-13",, -"2022-11-14",, -"2022-11-15",, -"2022-11-16",, -"2022-11-17",, -"2022-11-18",, -"2022-11-19",, -"2022-11-20",, -"2022-11-21",, -"2022-11-22",, -"2022-11-23",, -"2022-11-24",, -"2022-11-25",, -"2022-11-26",, -"2022-11-27",, -"2022-11-28",, -"2022-11-29",, -"2022-11-30",, -"2022-12-01",, -"2022-12-02",, -"2022-12-03",, -"2022-12-04",, -"2022-12-05",, -"2022-12-06",, -"2022-12-07",, -"2022-12-08",, -"2022-12-09",, -"2022-12-10",, -"2022-12-11",, -"2022-12-12",, -"2022-12-13",, -"2022-12-14",, -"2022-12-15",, -"2022-12-16",, -"2022-12-17",, -"2022-12-18",, -"2022-12-19",, -"2022-12-20",, -"2022-12-21",, -"2022-12-22",, -"2022-12-23",, -"2022-12-24",, -"2022-12-25",, -"2022-12-26",, -"2022-12-27",, -"2022-12-28",, -"2022-12-29",, -"2022-12-30",, -"2022-12-31",, -"2023-01-01",, -"2023-01-02",, -"2023-01-03",, -"2023-01-04",, -"2023-01-05",, -"2023-01-06",, -"2023-01-07",, -"2023-01-08",, -"2023-01-09",, -"2023-01-10",, -"2023-01-11",, -"2023-01-12",, -"2023-01-13",, -"2023-01-14",, -"2023-01-15",, -"2023-01-16",, -"2023-01-17",, -"2023-01-18",, -"2023-01-19",, -"2023-01-20",, -"2023-01-21",, -"2023-01-22",, -"2023-01-23",, -"2023-01-24",, -"2023-01-25",, -"2023-01-26",, -"2023-01-27",, -"2023-01-28",, -"2023-01-29",, -"2023-01-30",, -"2023-01-31",, -"2023-02-01",, -"2023-02-02",, -"2023-02-03",, -"2023-02-04",, -"2023-02-05",, -"2023-02-06",, -"2023-02-07",, -"2023-02-08",, -"2023-02-09",, -"2023-02-10",, -"2023-02-11",, -"2023-02-12",, -"2023-02-13",, -"2023-02-14",, -"2023-02-15",, -"2023-02-16",, -"2023-02-17",, -"2023-02-18",, -"2023-02-19",, -"2023-02-20",, -"2023-02-21",, -"2023-02-22",, -"2023-02-23",, -"2023-02-24",, -"2023-02-25",, -"2023-02-26",, -"2023-02-27",, -"2023-02-28",, -"2023-03-01",, -"2023-03-02",, -"2023-03-03",, -"2023-03-04",, -"2023-03-05",, -"2023-03-06",, -"2023-03-07",, -"2023-03-08",, -"2023-03-09",, -"2023-03-10",, -"2023-03-11",, -"2023-03-12",, -"2023-03-13",, -"2023-03-14",, -"2023-03-15",, -"2023-03-16",, -"2023-03-17",, -"2023-03-18",, -"2023-03-19",, -"2023-03-20",, -"2023-03-21",, -"2023-03-22",, -"2023-03-23",, -"2023-03-24",, -"2023-03-25",, -"2023-03-26",, -"2023-03-27",, -"2023-03-28",, -"2023-03-29",, -"2023-03-30",, -"2023-03-31",, -"2023-04-01",, -"2023-04-02",, -"2023-04-03",, -"2023-04-04",, -"2023-04-05",, -"2023-04-06",, -"2023-04-07",, -"2023-04-08",, -"2023-04-09",, -"2023-04-10",, -"2023-04-11",, -"2023-04-12",, -"2023-04-13",, -"2023-04-14",, -"2023-04-15",, -"2023-04-16",, -"2023-04-17",, -"2023-04-18",, -"2023-04-19",, -"2023-04-20",, -"2023-04-21",, -"2023-04-22",, -"2023-04-23",, -"2023-04-24",, -"2023-04-25",, -"2023-04-26",, -"2023-04-27",, -"2023-04-28",, -"2023-04-29",, -"2023-04-30",, -"2023-05-01",, -"2023-05-02",, -"2023-05-03",, -"2023-05-04",, -"2023-05-05",, -"2023-05-06",, -"2023-05-07",, -"2023-05-08",, -"2023-05-09",, -"2023-05-10",, -"2023-05-11",, -"2023-05-12",, -"2023-05-13",, -"2023-05-14",, -"2023-05-15",, -"2023-05-16",, -"2023-05-17",, -"2023-05-18",, -"2023-05-19",, -"2023-05-20",, -"2023-05-21",, -"2023-05-22",, -"2023-05-23",, -"2023-05-24",, -"2023-05-25",, -"2023-05-26",, -"2023-05-27",, -"2023-05-28",, -"2023-05-29",, -"2023-05-30",, -"2023-05-31",, -"2023-06-01",, -"2023-06-02",, -"2023-06-03",, -"2023-06-04",, -"2023-06-05",, -"2023-06-06",, -"2023-06-07",, -"2023-06-08",, -"2023-06-09",, -"2023-06-10",, -"2023-06-11",, -"2023-06-12",, -"2023-06-13",, -"2023-06-14",, -"2023-06-15",, -"2023-06-16",, -"2023-06-17",, -"2023-06-18",, -"2023-06-19",, -"2023-06-20",, -"2023-06-21",, -"2023-06-22",, -"2023-06-23",, -"2023-06-24",, -"2023-06-25",, -"2023-06-26",, -"2023-06-27",, -"2023-06-28",, -"2023-06-29",, -"2023-06-30",, -"2023-07-01",, -"2023-07-02",, -"2023-07-03",, -"2023-07-04",, -"2023-07-05",, -"2023-07-06",, -"2023-07-07",, -"2023-07-08",, -"2023-07-09",, -"2023-07-10",, -"2023-07-11",, -"2023-07-12",, -"2023-07-13",, -"2023-07-14",, -"2023-07-15",, -"2023-07-16",, -"2023-07-17",, -"2023-07-18",, -"2023-07-19",, -"2023-07-20",, -"2023-07-21",, -"2023-07-22",, -"2023-07-23",, -"2023-07-24",, -"2023-07-25",, -"2023-07-26",, -"2023-07-27",, -"2023-07-28",, -"2023-07-29",, -"2023-07-30",, -"2023-07-31",, \ No newline at end of file +"day","wday","lunch","dinner" +"2022-08-01","lunedì","https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html","https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" +"2022-08-02","martedì",, +"2022-08-03","mercoledì",, +"2022-08-04","giovedì",, +"2022-08-05","venerdì",, +"2022-08-06","sabato",, +"2022-08-07","domenica",, +"2022-08-08","lunedì",, +"2022-08-09","martedì",, +"2022-08-10","mercoledì",, +"2022-08-11","giovedì",, +"2022-08-12","venerdì",, +"2022-08-13","sabato",, +"2022-08-14","domenica",, +"2022-08-15","lunedì",, +"2022-08-16","martedì",, +"2022-08-17","mercoledì",, +"2022-08-18","giovedì",, +"2022-08-19","venerdì",, +"2022-08-20","sabato",, +"2022-08-21","domenica",, +"2022-08-22","lunedì",, +"2022-08-23","martedì",, +"2022-08-24","mercoledì",, +"2022-08-25","giovedì",, +"2022-08-26","venerdì",, +"2022-08-27","sabato",, +"2022-08-28","domenica",, +"2022-08-29","lunedì",, +"2022-08-30","martedì",, +"2022-08-31","mercoledì",, +"2022-09-01","giovedì",, +"2022-09-02","venerdì",, +"2022-09-03","sabato",, +"2022-09-04","domenica",, +"2022-09-05","lunedì",, +"2022-09-06","martedì",, +"2022-09-07","mercoledì",, +"2022-09-08","giovedì",, +"2022-09-09","venerdì",, +"2022-09-10","sabato",, +"2022-09-11","domenica",, +"2022-09-12","lunedì",, +"2022-09-13","martedì",, +"2022-09-14","mercoledì",, +"2022-09-15","giovedì",, +"2022-09-16","venerdì",, +"2022-09-17","sabato",, +"2022-09-18","domenica",, +"2022-09-19","lunedì",, +"2022-09-20","martedì",, +"2022-09-21","mercoledì",, +"2022-09-22","giovedì",, +"2022-09-23","venerdì",, +"2022-09-24","sabato",, +"2022-09-25","domenica",, +"2022-09-26","lunedì",, +"2022-09-27","martedì",, +"2022-09-28","mercoledì",, +"2022-09-29","giovedì",, +"2022-09-30","venerdì",, +"2022-10-01","sabato",, +"2022-10-02","domenica",, +"2022-10-03","lunedì",, +"2022-10-04","martedì",, +"2022-10-05","mercoledì",, +"2022-10-06","giovedì",, +"2022-10-07","venerdì",, +"2022-10-08","sabato",, +"2022-10-09","domenica",, +"2022-10-10","lunedì",, +"2022-10-11","martedì",, +"2022-10-12","mercoledì",, +"2022-10-13","giovedì",, +"2022-10-14","venerdì",, +"2022-10-15","sabato",, +"2022-10-16","domenica",, +"2022-10-17","lunedì",, +"2022-10-18","martedì",, +"2022-10-19","mercoledì",, +"2022-10-20","giovedì",, +"2022-10-21","venerdì",, +"2022-10-22","sabato",, +"2022-10-23","domenica",, +"2022-10-24","lunedì",, +"2022-10-25","martedì",, +"2022-10-26","mercoledì",, +"2022-10-27","giovedì",, +"2022-10-28","venerdì",, +"2022-10-29","sabato",, +"2022-10-30","domenica",, +"2022-10-31","lunedì",, +"2022-11-01","martedì",, +"2022-11-02","mercoledì",, +"2022-11-03","giovedì",, +"2022-11-04","venerdì",, +"2022-11-05","sabato",, +"2022-11-06","domenica",, +"2022-11-07","lunedì",, +"2022-11-08","martedì",, +"2022-11-09","mercoledì",, +"2022-11-10","giovedì",, +"2022-11-11","venerdì",, +"2022-11-12","sabato",, +"2022-11-13","domenica",, +"2022-11-14","lunedì",, +"2022-11-15","martedì",, +"2022-11-16","mercoledì",, +"2022-11-17","giovedì",, +"2022-11-18","venerdì",, +"2022-11-19","sabato",, +"2022-11-20","domenica",, +"2022-11-21","lunedì",, +"2022-11-22","martedì",, +"2022-11-23","mercoledì",, +"2022-11-24","giovedì",, +"2022-11-25","venerdì",, +"2022-11-26","sabato",, +"2022-11-27","domenica",, +"2022-11-28","lunedì",, +"2022-11-29","martedì",, +"2022-11-30","mercoledì",, +"2022-12-01","giovedì",, +"2022-12-02","venerdì",, +"2022-12-03","sabato",, +"2022-12-04","domenica",, +"2022-12-05","lunedì",, +"2022-12-06","martedì",, +"2022-12-07","mercoledì",, +"2022-12-08","giovedì",, +"2022-12-09","venerdì",, +"2022-12-10","sabato",, +"2022-12-11","domenica",, +"2022-12-12","lunedì",, +"2022-12-13","martedì",, +"2022-12-14","mercoledì",, +"2022-12-15","giovedì",, +"2022-12-16","venerdì",, +"2022-12-17","sabato",, +"2022-12-18","domenica",, +"2022-12-19","lunedì",, +"2022-12-20","martedì",, +"2022-12-21","mercoledì",, +"2022-12-22","giovedì",, +"2022-12-23","venerdì",, +"2022-12-24","sabato",, +"2022-12-25","domenica",, +"2022-12-26","lunedì",, +"2022-12-27","martedì",, +"2022-12-28","mercoledì",, +"2022-12-29","giovedì",, +"2022-12-30","venerdì",, +"2022-12-31","sabato",, +"2023-01-01","domenica",, +"2023-01-02","lunedì",, +"2023-01-03","martedì",, +"2023-01-04","mercoledì",, +"2023-01-05","giovedì",, +"2023-01-06","venerdì",, +"2023-01-07","sabato",, +"2023-01-08","domenica",, +"2023-01-09","lunedì",, +"2023-01-10","martedì",, +"2023-01-11","mercoledì",, +"2023-01-12","giovedì",, +"2023-01-13","venerdì",, +"2023-01-14","sabato",, +"2023-01-15","domenica",, +"2023-01-16","lunedì",, +"2023-01-17","martedì",, +"2023-01-18","mercoledì",, +"2023-01-19","giovedì",, +"2023-01-20","venerdì",, +"2023-01-21","sabato",, +"2023-01-22","domenica",, +"2023-01-23","lunedì",, +"2023-01-24","martedì",, +"2023-01-25","mercoledì",, +"2023-01-26","giovedì",, +"2023-01-27","venerdì",, +"2023-01-28","sabato",, +"2023-01-29","domenica",, +"2023-01-30","lunedì",, +"2023-01-31","martedì",, +"2023-02-01","mercoledì",, +"2023-02-02","giovedì",, +"2023-02-03","venerdì",, +"2023-02-04","sabato",, +"2023-02-05","domenica",, +"2023-02-06","lunedì",, +"2023-02-07","martedì",, +"2023-02-08","mercoledì",, +"2023-02-09","giovedì",, +"2023-02-10","venerdì",, +"2023-02-11","sabato",, +"2023-02-12","domenica",, +"2023-02-13","lunedì",, +"2023-02-14","martedì",, +"2023-02-15","mercoledì",, +"2023-02-16","giovedì",, +"2023-02-17","venerdì",, +"2023-02-18","sabato",, +"2023-02-19","domenica",, +"2023-02-20","lunedì",, +"2023-02-21","martedì",, +"2023-02-22","mercoledì",, +"2023-02-23","giovedì",, +"2023-02-24","venerdì",, +"2023-02-25","sabato",, +"2023-02-26","domenica",, +"2023-02-27","lunedì",, +"2023-02-28","martedì",, +"2023-03-01","mercoledì",, +"2023-03-02","giovedì",, +"2023-03-03","venerdì",, +"2023-03-04","sabato",, +"2023-03-05","domenica",, +"2023-03-06","lunedì",, +"2023-03-07","martedì",, +"2023-03-08","mercoledì",, +"2023-03-09","giovedì",, +"2023-03-10","venerdì",, +"2023-03-11","sabato",, +"2023-03-12","domenica",, +"2023-03-13","lunedì",, +"2023-03-14","martedì",, +"2023-03-15","mercoledì",, +"2023-03-16","giovedì",, +"2023-03-17","venerdì",, +"2023-03-18","sabato",, +"2023-03-19","domenica",, +"2023-03-20","lunedì",, +"2023-03-21","martedì",, +"2023-03-22","mercoledì",, +"2023-03-23","giovedì",, +"2023-03-24","venerdì",, +"2023-03-25","sabato",, +"2023-03-26","domenica",, +"2023-03-27","lunedì",, +"2023-03-28","martedì",, +"2023-03-29","mercoledì",, +"2023-03-30","giovedì",, +"2023-03-31","venerdì",, +"2023-04-01","sabato",, +"2023-04-02","domenica",, +"2023-04-03","lunedì",, +"2023-04-04","martedì",, +"2023-04-05","mercoledì",, +"2023-04-06","giovedì",, +"2023-04-07","venerdì",, +"2023-04-08","sabato",, +"2023-04-09","domenica",, +"2023-04-10","lunedì",, +"2023-04-11","martedì",, +"2023-04-12","mercoledì",, +"2023-04-13","giovedì",, +"2023-04-14","venerdì",, +"2023-04-15","sabato",, +"2023-04-16","domenica",, +"2023-04-17","lunedì",, +"2023-04-18","martedì",, +"2023-04-19","mercoledì",, +"2023-04-20","giovedì",, +"2023-04-21","venerdì",, +"2023-04-22","sabato",, +"2023-04-23","domenica",, +"2023-04-24","lunedì",, +"2023-04-25","martedì",, +"2023-04-26","mercoledì",, +"2023-04-27","giovedì",, +"2023-04-28","venerdì",, +"2023-04-29","sabato",, +"2023-04-30","domenica",, +"2023-05-01","lunedì",, +"2023-05-02","martedì",, +"2023-05-03","mercoledì",, +"2023-05-04","giovedì",, +"2023-05-05","venerdì",, +"2023-05-06","sabato",, +"2023-05-07","domenica",, +"2023-05-08","lunedì",, +"2023-05-09","martedì",, +"2023-05-10","mercoledì",, +"2023-05-11","giovedì",, +"2023-05-12","venerdì",, +"2023-05-13","sabato",, +"2023-05-14","domenica",, +"2023-05-15","lunedì",, +"2023-05-16","martedì",, +"2023-05-17","mercoledì",, +"2023-05-18","giovedì",, +"2023-05-19","venerdì",, +"2023-05-20","sabato",, +"2023-05-21","domenica",, +"2023-05-22","lunedì",, +"2023-05-23","martedì",, +"2023-05-24","mercoledì",, +"2023-05-25","giovedì",, +"2023-05-26","venerdì",, +"2023-05-27","sabato",, +"2023-05-28","domenica",, +"2023-05-29","lunedì",, +"2023-05-30","martedì",, +"2023-05-31","mercoledì",, +"2023-06-01","giovedì",, +"2023-06-02","venerdì",, +"2023-06-03","sabato",, +"2023-06-04","domenica",, +"2023-06-05","lunedì",, +"2023-06-06","martedì",, +"2023-06-07","mercoledì",, +"2023-06-08","giovedì",, +"2023-06-09","venerdì",, +"2023-06-10","sabato",, +"2023-06-11","domenica",, +"2023-06-12","lunedì",, +"2023-06-13","martedì",, +"2023-06-14","mercoledì",, +"2023-06-15","giovedì",, +"2023-06-16","venerdì",, +"2023-06-17","sabato",, +"2023-06-18","domenica",, +"2023-06-19","lunedì",, +"2023-06-20","martedì",, +"2023-06-21","mercoledì",, +"2023-06-22","giovedì",, +"2023-06-23","venerdì",, +"2023-06-24","sabato",, +"2023-06-25","domenica",, +"2023-06-26","lunedì",, +"2023-06-27","martedì",, +"2023-06-28","mercoledì",, +"2023-06-29","giovedì",, +"2023-06-30","venerdì",, +"2023-07-01","sabato",, +"2023-07-02","domenica",, +"2023-07-03","lunedì",, +"2023-07-04","martedì",, +"2023-07-05","mercoledì",, +"2023-07-06","giovedì",, +"2023-07-07","venerdì",, +"2023-07-08","sabato",, +"2023-07-09","domenica",, +"2023-07-10","lunedì",, +"2023-07-11","martedì",, +"2023-07-12","mercoledì",, +"2023-07-13","giovedì",, +"2023-07-14","venerdì",, +"2023-07-15","sabato",, +"2023-07-16","domenica",, +"2023-07-17","lunedì",, +"2023-07-18","martedì",, +"2023-07-19","mercoledì",, +"2023-07-20","giovedì",, +"2023-07-21","venerdì",, +"2023-07-22","sabato",, +"2023-07-23","domenica",, +"2023-07-24","lunedì",, +"2023-07-25","martedì",, +"2023-07-26","mercoledì",, +"2023-07-27","giovedì",, +"2023-07-28","venerdì",, +"2023-07-29","sabato",, +"2023-07-30","domenica",, +"2023-07-31","lunedì",, diff --git a/src/event.rs b/src/event.rs index e79a379..19a7923 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,7 +3,8 @@ use { crate::recipe::Recipe, - chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}, + chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}, + ics::escape_text, ics::properties as calprop, ics::Event as CalEvent, std::rc::Rc, @@ -49,13 +50,13 @@ impl<'a> From for CalEvent<'a> { fn from(ev: Event) -> Self { let start_time = ev.ends_at - ev.recipe.total_time(); - let mut event = ics::Event::new(ev.uid.clone(), dt_fmt(&Local::now())); - event.push(calprop::Summary::new(ev.recipe.name.clone())); + let mut event = ics::Event::new(ev.uid.clone(), dt_utc_fmt(&Utc::now())); + event.push(calprop::Summary::new(escape_text(ev.recipe.name.clone()))); event.push(calprop::Description::new(format!( "cookbook@{}", ev.recipe.id ))); - event.push(calprop::Location::new(ev.recipe.url.clone())); + event.push(calprop::Location::new(escape_text(ev.recipe.url.clone()))); event.push(calprop::DtStart::new(dt_fmt( &Local.from_local_datetime(&start_time).unwrap(), ))); @@ -68,7 +69,7 @@ impl<'a> From for CalEvent<'a> { let alarm = ics::Alarm::display( calprop::Trigger::new("-P15M"), - calprop::Description::new(ev.recipe.name.clone()), + calprop::Description::new(escape_text(ev.recipe.name.clone())), ); event.add_alarm(alarm); event @@ -78,3 +79,7 @@ impl<'a> From for CalEvent<'a> { fn dt_fmt(datetime: &DateTime) -> String { datetime.format("%Y%m%dT%H%M%S").to_string() } + +fn dt_utc_fmt(datetime: &DateTime) -> String { + datetime.format("%Y%m%dT%H%M%SZ").to_string() +} From 77404d1642af99e26fe764a9f451b4626acb16c6 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Fri, 29 Jul 2022 19:52:50 +0200 Subject: [PATCH 12/13] Fix outputting tzid and alarm trigger --- Cargo.lock | 71 ++++++++++++------------------------ Cargo.toml | 8 ++-- src/api_client.rs | 63 +++++++++++++++++++++++++++++--- src/commands/import.rs | 4 +- src/commands/schedule.rs | 4 +- src/commands/schedule_csv.rs | 21 ++++++----- src/event.rs | 24 ++++++++---- 7 files changed, 117 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a83d89..6bb7b82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.58" @@ -160,6 +169,7 @@ dependencies = [ "csv", "directories", "futures", + "iana-time-zone", "ics", "minicaldav", "reqwest", @@ -170,7 +180,7 @@ dependencies = [ "strum_macros", "tokio", "toml", - "uuid", + "ureq", "webbrowser", ] @@ -571,6 +581,19 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c0d80ad9ca8d30ca648bf6cb1e3e3326d75071b76dbe143dd4a9cedcd58975" +dependencies = [ + "android_system_properties", + "core-foundation", + "js-sys", + "wasm-bindgen", + "winapi", +] + [[package]] name = "ics" version = "0.5.7" @@ -944,12 +967,6 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" -[[package]] -name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -978,36 +995,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - [[package]] name = "redox_syscall" version = "0.2.13" @@ -1529,16 +1516,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "uuid" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" -dependencies = [ - "getrandom", - "rand", -] - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 9ac7e8c..06b7af8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ version = "0.13" version = "0.4" features = ["serde"] +[dependencies.iana-time-zone] +version = "0.1" + [dependencies.csv] version = "1.1" @@ -69,9 +72,8 @@ version = "0.5" version = "1" features = ["rt-multi-thread", "net", "macros"] -[dependencies.uuid] -version = "1.1" -features = ["v4", "fast-rng"] +[dependencies.ureq] +version = "2.5" [dependencies.webbrowser] version = "0.7" diff --git a/src/api_client.rs b/src/api_client.rs index 6acb3e4..33d7c43 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -2,18 +2,21 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use { - crate::config::Config, crate::constants, anyhow::anyhow, + crate::config::Config, crate::constants, anyhow::anyhow, anyhow::Result, base64::write::EncoderWriter as Base64Encoder, reqwest::Url, std::io::Write, }; pub struct ApiClient { - pub base_url: Url, - pub username: String, - pub rest: reqwest::Client, + rest: reqwest::Client, + agent: ureq::Agent, + base_url: Url, + caldav_base_url: Url, + username: String, + password: String, } impl ApiClient { - pub fn new(server_name: &str, configuration: &Config) -> anyhow::Result { + pub fn new(server_name: &str, configuration: &Config) -> Result { let server = configuration .credentials .servers @@ -43,10 +46,58 @@ impl ApiClient { .default_headers(default_headers) .build()?; + let base_url = Url::parse(&server.url)?; + let caldav_base_url = futures::executor::block_on( + rest_client + .head(base_url.join("/.well-known/caldav")?) + .send(), + )? + .url() + .clone(); + Ok(ApiClient { - base_url: Url::parse(&server.url)?, + base_url, + caldav_base_url, username: server.login_name.clone(), + password: server.password.clone(), rest: rest_client, + agent: ureq::Agent::new(), }) } + + pub fn rest(&self) -> &reqwest::Client { + &self.rest + } + + pub fn base_url(&self) -> &Url { + &self.base_url + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn get_calendars( + &self, + ) -> core::result::Result, minicaldav::Error> { + minicaldav::get_calendars( + self.agent.clone(), + self.username(), + &self.password, + &self.caldav_base_url, + ) + } + + pub fn get_events( + &self, + calendar: &minicaldav::Calendar, + ) -> core::result::Result<(Vec, Vec), minicaldav::Error> + { + minicaldav::get_events( + self.agent.clone(), + self.username(), + &self.password, + calendar, + ) + } } diff --git a/src/commands/import.rs b/src/commands/import.rs index 3ac8331..f7c0e82 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -10,8 +10,8 @@ where { for url in urls { let response = api_client - .rest - .post(api_client.base_url.join("apps/cookbook/import")?) + .rest() + .post(api_client.base_url().join("apps/cookbook/import")?) .json(&serde_json::json!({ "url": url.as_ref(), })) diff --git a/src/commands/schedule.rs b/src/commands/schedule.rs index f92cab2..529b660 100644 --- a/src/commands/schedule.rs +++ b/src/commands/schedule.rs @@ -5,8 +5,8 @@ use {crate::api_client::ApiClient, crate::recipe, anyhow::Result}; pub async fn with(api_client: &ApiClient) -> Result<()> { let recipes = api_client - .rest - .get(api_client.base_url.join("apps/cookbook/api/recipes")?) + .rest() + .get(api_client.base_url().join("apps/cookbook/api/recipes")?) .send() .await?; println!("{:#?}", recipes.json::>().await?); diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index 4fa0c44..40769fa 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -71,8 +71,8 @@ where async fn get_all_recipes(api_client: &ApiClient) -> Result>> { let metadata = api_client - .rest - .get(api_client.base_url.join("apps/cookbook/api/recipes")?) + .rest() + .get(api_client.base_url().join("apps/cookbook/api/recipes")?) .send() .await? .json::>() @@ -80,10 +80,10 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result From for CalEvent<'a> { fn from(ev: Event) -> Self { - let start_time = ev.ends_at - ev.recipe.total_time(); + let start_time = Local + .from_local_datetime(&(*&ev.ends_at - ev.recipe.total_time())) + .unwrap(); + let end_time = Local.from_local_datetime(&ev.ends_at).unwrap(); + let timezone = iana_time_zone::get_timezone().unwrap(); let mut event = ics::Event::new(ev.uid.clone(), dt_utc_fmt(&Utc::now())); event.push(calprop::Summary::new(escape_text(ev.recipe.name.clone()))); @@ -57,18 +61,22 @@ impl<'a> From for CalEvent<'a> { ev.recipe.id ))); event.push(calprop::Location::new(escape_text(ev.recipe.url.clone()))); - event.push(calprop::DtStart::new(dt_fmt( - &Local.from_local_datetime(&start_time).unwrap(), - ))); - event.push(calprop::DtEnd::new(dt_fmt( - &Local.from_local_datetime(&ev.ends_at).unwrap(), - ))); + + let mut dtstart = calprop::DtStart::new(dt_fmt(&start_time)); + dtstart.append(ics::parameters!("TZID" => timezone.clone())); + event.push(dtstart); + + let mut dtend = calprop::DtEnd::new(dt_fmt(&end_time)); + dtend.append(ics::parameters!("TZID" => timezone)); + event.push(dtend); // TODO make configurable yearly repetition, for now this suits my personal uses event.push(calprop::RRule::new("FREQ=YEARLY")); + let mut trigger = calprop::Trigger::new("-PT15M"); + trigger.append(ics::parameters!("RELATED" => "START")); let alarm = ics::Alarm::display( - calprop::Trigger::new("-P15M"), + trigger, calprop::Description::new(escape_text(ev.recipe.name.clone())), ); event.add_alarm(alarm); From 7529556ea6ef54095684bad7e0384a6cb05d13c9 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Fri, 29 Jul 2022 22:56:17 +0200 Subject: [PATCH 13/13] Make recurrent events preserve weekday across years --- examples/example-schedule.csv | 430 +++++++++++++++++----------------- src/event.rs | 11 +- 2 files changed, 224 insertions(+), 217 deletions(-) diff --git a/examples/example-schedule.csv b/examples/example-schedule.csv index e710405..e0ca5eb 100644 --- a/examples/example-schedule.csv +++ b/examples/example-schedule.csv @@ -1,5 +1,217 @@ "day","wday","lunch","dinner" -"2022-08-01","lunedì","https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html","https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" +2022-01-01,"sabato",, +2022-01-02,"domenica",, +2022-01-03,"lunedì",, +2022-01-04,"martedì",, +2022-01-05,"mercoledì",, +2022-01-06,"giovedì",, +2022-01-07,"venerdì",, +2022-01-08,"sabato",, +2022-01-09,"domenica",, +2022-01-10,"lunedì",, +2022-01-11,"martedì",, +2022-01-12,"mercoledì",, +2022-01-13,"giovedì",, +2022-01-14,"venerdì",, +2022-01-15,"sabato",, +2022-01-16,"domenica",, +2022-01-17,"lunedì",, +2022-01-18,"martedì",, +2022-01-19,"mercoledì",, +2022-01-20,"giovedì",, +2022-01-21,"venerdì",, +2022-01-22,"sabato",, +2022-01-23,"domenica",, +2022-01-24,"lunedì",, +2022-01-25,"martedì",, +2022-01-26,"mercoledì",, +2022-01-27,"giovedì",, +2022-01-28,"venerdì",, +2022-01-29,"sabato",, +2022-01-30,"domenica",, +2022-01-31,"lunedì",, +2022-02-01,"martedì",, +2022-02-02,"mercoledì",, +2022-02-03,"giovedì",, +2022-02-04,"venerdì",, +2022-02-05,"sabato",, +2022-02-06,"domenica",, +2022-02-07,"lunedì",, +2022-02-08,"martedì",, +2022-02-09,"mercoledì",, +2022-02-10,"giovedì",, +2022-02-11,"venerdì",, +2022-02-12,"sabato",, +2022-02-13,"domenica",, +2022-02-14,"lunedì",, +2022-02-15,"martedì",, +2022-02-16,"mercoledì",, +2022-02-17,"giovedì",, +2022-02-18,"venerdì",, +2022-02-19,"sabato",, +2022-02-20,"domenica",, +2022-02-21,"lunedì",, +2022-02-22,"martedì",, +2022-02-23,"mercoledì",, +2022-02-24,"giovedì",, +2022-02-25,"venerdì",, +2022-02-26,"sabato",, +2022-02-27,"domenica",, +2022-02-28,"lunedì",, +2022-03-01,"martedì",, +2022-03-02,"mercoledì",, +2022-03-03,"giovedì",, +2022-03-04,"venerdì",, +2022-03-05,"sabato",, +2022-03-06,"domenica",, +2022-03-07,"lunedì",, +2022-03-08,"martedì",, +2022-03-09,"mercoledì",, +2022-03-10,"giovedì",, +2022-03-11,"venerdì",, +2022-03-12,"sabato",, +2022-03-13,"domenica",, +2022-03-14,"lunedì",, +2022-03-15,"martedì",, +2022-03-16,"mercoledì",, +2022-03-17,"giovedì",, +2022-03-18,"venerdì",, +2022-03-19,"sabato",, +2022-03-20,"domenica",, +2022-03-21,"lunedì",, +2022-03-22,"martedì",, +2022-03-23,"mercoledì",, +2022-03-24,"giovedì",, +2022-03-25,"venerdì",, +2022-03-26,"sabato",, +2022-03-27,"domenica",, +2022-03-28,"lunedì",, +2022-03-29,"martedì",, +2022-03-30,"mercoledì",, +2022-03-31,"giovedì",, +2022-04-01,"venerdì",, +2022-04-02,"sabato",, +2022-04-03,"domenica",, +2022-04-04,"lunedì",, +2022-04-05,"martedì",, +2022-04-06,"mercoledì",, +2022-04-07,"giovedì",, +2022-04-08,"venerdì",, +2022-04-09,"sabato",, +2022-04-10,"domenica",, +2022-04-11,"lunedì",, +2022-04-12,"martedì",, +2022-04-13,"mercoledì",, +2022-04-14,"giovedì",, +2022-04-15,"venerdì",, +2022-04-16,"sabato",, +2022-04-17,"domenica",, +2022-04-18,"lunedì",, +2022-04-19,"martedì",, +2022-04-20,"mercoledì",, +2022-04-21,"giovedì",, +2022-04-22,"venerdì",, +2022-04-23,"sabato",, +2022-04-24,"domenica",, +2022-04-25,"lunedì",, +2022-04-26,"martedì",, +2022-04-27,"mercoledì",, +2022-04-28,"giovedì",, +2022-04-29,"venerdì",, +2022-04-30,"sabato",, +2022-05-01,"domenica",, +2022-05-02,"lunedì",, +2022-05-03,"martedì",, +2022-05-04,"mercoledì",, +2022-05-05,"giovedì",, +2022-05-06,"venerdì",, +2022-05-07,"sabato",, +2022-05-08,"domenica",, +2022-05-09,"lunedì",, +2022-05-10,"martedì",, +2022-05-11,"mercoledì",, +2022-05-12,"giovedì",, +2022-05-13,"venerdì",, +2022-05-14,"sabato",, +2022-05-15,"domenica",, +2022-05-16,"lunedì",, +2022-05-17,"martedì",, +2022-05-18,"mercoledì",, +2022-05-19,"giovedì",, +2022-05-20,"venerdì",, +2022-05-21,"sabato",, +2022-05-22,"domenica",, +2022-05-23,"lunedì",, +2022-05-24,"martedì",, +2022-05-25,"mercoledì",, +2022-05-26,"giovedì",, +2022-05-27,"venerdì",, +2022-05-28,"sabato",, +2022-05-29,"domenica",, +2022-05-30,"lunedì",, +2022-05-31,"martedì",, +2022-06-01,"mercoledì",, +2022-06-02,"giovedì",, +2022-06-03,"venerdì",, +2022-06-04,"sabato",, +2022-06-05,"domenica",, +2022-06-06,"lunedì",, +2022-06-07,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +2022-06-08,"mercoledì",, +2022-06-09,"giovedì",, +2022-06-10,"venerdì",, +2022-06-11,"sabato",, +2022-06-12,"domenica",, +2022-06-13,"lunedì",, +2022-06-14,"martedì",, +2022-06-15,"mercoledì",, +2022-06-16,"giovedì",, +2022-06-17,"venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html" +2022-06-18,"sabato",, +2022-06-19,"domenica",, +2022-06-20,"lunedì",, +2022-06-21,"martedì",, +2022-06-22,"mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +2022-06-23,"giovedì",, +2022-06-24,"venerdì",, +2022-06-25,"sabato",, +2022-06-26,"domenica",, +2022-06-27,"lunedì",, +2022-06-28,"martedì",, +2022-06-29,"mercoledì",, +2022-06-30,"giovedì",, +2022-07-01,"venerdì",, +2022-07-02,"sabato",, +2022-07-03,"domenica",, +2022-07-04,"lunedì",, +2022-07-05,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +2022-07-06,"mercoledì",, +2022-07-07,"giovedì",, +2022-07-08,"venerdì",, +2022-07-09,"sabato",, +2022-07-10,"domenica",, +2022-07-11,"lunedì",, +2022-07-12,"martedì",, +2022-07-13,"mercoledì",, +2022-07-14,"giovedì",, +2022-07-15,"venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html" +2022-07-16,"sabato",, +2022-07-17,"domenica",, +2022-07-18,"lunedì",, +2022-07-19,"martedì",, +2022-07-20,"mercoledì",, +2022-07-21,"giovedì",, +2022-07-22,"venerdì",, +2022-07-23,"sabato",, +2022-07-24,"domenica",, +2022-07-25,"lunedì",, +2022-07-26,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +2022-07-27,"mercoledì",, +2022-07-28,"giovedì",, +2022-07-29,"venerdì",, +2022-07-30,"sabato",, +2022-07-31,"domenica",, +"2022-08-01","lunedì",,"https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" "2022-08-02","martedì",, "2022-08-03","mercoledì",, "2022-08-04","giovedì",, @@ -15,10 +227,10 @@ "2022-08-14","domenica",, "2022-08-15","lunedì",, "2022-08-16","martedì",, -"2022-08-17","mercoledì",, +"2022-08-17","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" "2022-08-18","giovedì",, "2022-08-19","venerdì",, -"2022-08-20","sabato",, +"2022-08-20","sabato",,"https://ricette.giallozafferano.it/Salmorejo.html" "2022-08-21","domenica",, "2022-08-22","lunedì",, "2022-08-23","martedì",, @@ -152,215 +364,3 @@ "2022-12-29","giovedì",, "2022-12-30","venerdì",, "2022-12-31","sabato",, -"2023-01-01","domenica",, -"2023-01-02","lunedì",, -"2023-01-03","martedì",, -"2023-01-04","mercoledì",, -"2023-01-05","giovedì",, -"2023-01-06","venerdì",, -"2023-01-07","sabato",, -"2023-01-08","domenica",, -"2023-01-09","lunedì",, -"2023-01-10","martedì",, -"2023-01-11","mercoledì",, -"2023-01-12","giovedì",, -"2023-01-13","venerdì",, -"2023-01-14","sabato",, -"2023-01-15","domenica",, -"2023-01-16","lunedì",, -"2023-01-17","martedì",, -"2023-01-18","mercoledì",, -"2023-01-19","giovedì",, -"2023-01-20","venerdì",, -"2023-01-21","sabato",, -"2023-01-22","domenica",, -"2023-01-23","lunedì",, -"2023-01-24","martedì",, -"2023-01-25","mercoledì",, -"2023-01-26","giovedì",, -"2023-01-27","venerdì",, -"2023-01-28","sabato",, -"2023-01-29","domenica",, -"2023-01-30","lunedì",, -"2023-01-31","martedì",, -"2023-02-01","mercoledì",, -"2023-02-02","giovedì",, -"2023-02-03","venerdì",, -"2023-02-04","sabato",, -"2023-02-05","domenica",, -"2023-02-06","lunedì",, -"2023-02-07","martedì",, -"2023-02-08","mercoledì",, -"2023-02-09","giovedì",, -"2023-02-10","venerdì",, -"2023-02-11","sabato",, -"2023-02-12","domenica",, -"2023-02-13","lunedì",, -"2023-02-14","martedì",, -"2023-02-15","mercoledì",, -"2023-02-16","giovedì",, -"2023-02-17","venerdì",, -"2023-02-18","sabato",, -"2023-02-19","domenica",, -"2023-02-20","lunedì",, -"2023-02-21","martedì",, -"2023-02-22","mercoledì",, -"2023-02-23","giovedì",, -"2023-02-24","venerdì",, -"2023-02-25","sabato",, -"2023-02-26","domenica",, -"2023-02-27","lunedì",, -"2023-02-28","martedì",, -"2023-03-01","mercoledì",, -"2023-03-02","giovedì",, -"2023-03-03","venerdì",, -"2023-03-04","sabato",, -"2023-03-05","domenica",, -"2023-03-06","lunedì",, -"2023-03-07","martedì",, -"2023-03-08","mercoledì",, -"2023-03-09","giovedì",, -"2023-03-10","venerdì",, -"2023-03-11","sabato",, -"2023-03-12","domenica",, -"2023-03-13","lunedì",, -"2023-03-14","martedì",, -"2023-03-15","mercoledì",, -"2023-03-16","giovedì",, -"2023-03-17","venerdì",, -"2023-03-18","sabato",, -"2023-03-19","domenica",, -"2023-03-20","lunedì",, -"2023-03-21","martedì",, -"2023-03-22","mercoledì",, -"2023-03-23","giovedì",, -"2023-03-24","venerdì",, -"2023-03-25","sabato",, -"2023-03-26","domenica",, -"2023-03-27","lunedì",, -"2023-03-28","martedì",, -"2023-03-29","mercoledì",, -"2023-03-30","giovedì",, -"2023-03-31","venerdì",, -"2023-04-01","sabato",, -"2023-04-02","domenica",, -"2023-04-03","lunedì",, -"2023-04-04","martedì",, -"2023-04-05","mercoledì",, -"2023-04-06","giovedì",, -"2023-04-07","venerdì",, -"2023-04-08","sabato",, -"2023-04-09","domenica",, -"2023-04-10","lunedì",, -"2023-04-11","martedì",, -"2023-04-12","mercoledì",, -"2023-04-13","giovedì",, -"2023-04-14","venerdì",, -"2023-04-15","sabato",, -"2023-04-16","domenica",, -"2023-04-17","lunedì",, -"2023-04-18","martedì",, -"2023-04-19","mercoledì",, -"2023-04-20","giovedì",, -"2023-04-21","venerdì",, -"2023-04-22","sabato",, -"2023-04-23","domenica",, -"2023-04-24","lunedì",, -"2023-04-25","martedì",, -"2023-04-26","mercoledì",, -"2023-04-27","giovedì",, -"2023-04-28","venerdì",, -"2023-04-29","sabato",, -"2023-04-30","domenica",, -"2023-05-01","lunedì",, -"2023-05-02","martedì",, -"2023-05-03","mercoledì",, -"2023-05-04","giovedì",, -"2023-05-05","venerdì",, -"2023-05-06","sabato",, -"2023-05-07","domenica",, -"2023-05-08","lunedì",, -"2023-05-09","martedì",, -"2023-05-10","mercoledì",, -"2023-05-11","giovedì",, -"2023-05-12","venerdì",, -"2023-05-13","sabato",, -"2023-05-14","domenica",, -"2023-05-15","lunedì",, -"2023-05-16","martedì",, -"2023-05-17","mercoledì",, -"2023-05-18","giovedì",, -"2023-05-19","venerdì",, -"2023-05-20","sabato",, -"2023-05-21","domenica",, -"2023-05-22","lunedì",, -"2023-05-23","martedì",, -"2023-05-24","mercoledì",, -"2023-05-25","giovedì",, -"2023-05-26","venerdì",, -"2023-05-27","sabato",, -"2023-05-28","domenica",, -"2023-05-29","lunedì",, -"2023-05-30","martedì",, -"2023-05-31","mercoledì",, -"2023-06-01","giovedì",, -"2023-06-02","venerdì",, -"2023-06-03","sabato",, -"2023-06-04","domenica",, -"2023-06-05","lunedì",, -"2023-06-06","martedì",, -"2023-06-07","mercoledì",, -"2023-06-08","giovedì",, -"2023-06-09","venerdì",, -"2023-06-10","sabato",, -"2023-06-11","domenica",, -"2023-06-12","lunedì",, -"2023-06-13","martedì",, -"2023-06-14","mercoledì",, -"2023-06-15","giovedì",, -"2023-06-16","venerdì",, -"2023-06-17","sabato",, -"2023-06-18","domenica",, -"2023-06-19","lunedì",, -"2023-06-20","martedì",, -"2023-06-21","mercoledì",, -"2023-06-22","giovedì",, -"2023-06-23","venerdì",, -"2023-06-24","sabato",, -"2023-06-25","domenica",, -"2023-06-26","lunedì",, -"2023-06-27","martedì",, -"2023-06-28","mercoledì",, -"2023-06-29","giovedì",, -"2023-06-30","venerdì",, -"2023-07-01","sabato",, -"2023-07-02","domenica",, -"2023-07-03","lunedì",, -"2023-07-04","martedì",, -"2023-07-05","mercoledì",, -"2023-07-06","giovedì",, -"2023-07-07","venerdì",, -"2023-07-08","sabato",, -"2023-07-09","domenica",, -"2023-07-10","lunedì",, -"2023-07-11","martedì",, -"2023-07-12","mercoledì",, -"2023-07-13","giovedì",, -"2023-07-14","venerdì",, -"2023-07-15","sabato",, -"2023-07-16","domenica",, -"2023-07-17","lunedì",, -"2023-07-18","martedì",, -"2023-07-19","mercoledì",, -"2023-07-20","giovedì",, -"2023-07-21","venerdì",, -"2023-07-22","sabato",, -"2023-07-23","domenica",, -"2023-07-24","lunedì",, -"2023-07-25","martedì",, -"2023-07-26","mercoledì",, -"2023-07-27","giovedì",, -"2023-07-28","venerdì",, -"2023-07-29","sabato",, -"2023-07-30","domenica",, -"2023-07-31","lunedì",, diff --git a/src/event.rs b/src/event.rs index 7c1df4e..03c8f50 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,7 +3,7 @@ use { crate::recipe::Recipe, - chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}, + chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}, ics::escape_text, ics::properties as calprop, ics::Event as CalEvent, @@ -71,7 +71,14 @@ impl<'a> From for CalEvent<'a> { event.push(dtend); // TODO make configurable yearly repetition, for now this suits my personal uses - event.push(calprop::RRule::new("FREQ=YEARLY")); + const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; + event.push(calprop::RRule::new(format!( + "FREQ=YEARLY;BYDAY={weekday};BYWEEKNO={weekno}", + weekday = DAY_NAMES + .get(start_time.weekday().num_days_from_monday() as usize) + .unwrap(), + weekno = start_time.iso_week().week(), + ))); let mut trigger = calprop::Trigger::new("-PT15M"); trigger.append(ics::parameters!("RELATED" => "START"));