Use only the icalendar crate instead of ics
This commit is contained in:
parent
ed5a6d2306
commit
1510eb4f3d
|
@ -6,7 +6,7 @@
|
|||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'cook'",
|
||||
"name": "Debug executable 'cook' -> groceries",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
|
@ -21,6 +21,24 @@
|
|||
"args": ["groceries", "Cucina"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'cook' -> schedule-csv",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=cook",
|
||||
"--package=cooking-schedule"
|
||||
],
|
||||
"filter": {
|
||||
"name": "cook",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": ["schedule-csv", "Cucina", "examples/example-schedule.csv"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
|
|
|
@ -11,15 +11,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -166,11 +157,8 @@ dependencies = [
|
|||
"csv",
|
||||
"directories",
|
||||
"futures",
|
||||
"iana-time-zone",
|
||||
"icalendar",
|
||||
"ics",
|
||||
"libxml",
|
||||
"markdown-gen",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rusty-hook",
|
||||
|
@ -561,19 +549,6 @@ 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 = "icalendar"
|
||||
version = "0.13.0"
|
||||
|
@ -585,12 +560,6 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -706,12 +675,6 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-gen"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034621d7f1258317ca1dfb9205e3925d27ee4aa2a46620a09c567daf0310562"
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.9"
|
||||
|
|
|
@ -26,9 +26,6 @@ version = "0.13"
|
|||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.iana-time-zone]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.csv]
|
||||
version = "1.1"
|
||||
|
||||
|
@ -46,15 +43,9 @@ version = "0.3"
|
|||
version = "0.13.0"
|
||||
features = ["parser"]
|
||||
|
||||
[dependencies.ics]
|
||||
version = "0.5"
|
||||
|
||||
[dependencies.libxml]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.markdown-gen]
|
||||
version = "1.2"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "1.6"
|
||||
|
||||
|
|
|
@ -15,12 +15,13 @@ use {
|
|||
pub async fn with(api_client: &ApiClient, calendar_name: &str, days: u32) -> Result<()> {
|
||||
let ids = map_events_to_recipe_ids(api_client, calendar_name, days).await?;
|
||||
let ingredients = get_ingredients(api_client, ids).await?;
|
||||
let ingredients = merge_ingredients(ingredients);
|
||||
let md = prepare_grocery_list(&ingredients)?;
|
||||
|
||||
todo!("Recipe ingredients: {:?}", ingredients)
|
||||
println!("{}", md);
|
||||
|
||||
// let ingredients = merge_ingredients(ingredients);
|
||||
// let md = prepare_grocery_list(&ingredients);
|
||||
// save_grocery_list(api_client, "", md).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn map_events_to_recipe_ids(
|
||||
|
@ -70,3 +71,28 @@ where
|
|||
let ingredients = futures::future::try_join_all(ingredients).await?;
|
||||
Ok(ingredients.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
fn merge_ingredients(mut ingredients: Vec<Ingredient>) -> Vec<Ingredient> {
|
||||
ingredients.sort();
|
||||
|
||||
// TODO actual merging
|
||||
|
||||
ingredients
|
||||
}
|
||||
|
||||
fn prepare_grocery_list(ingredients: &Vec<Ingredient>) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
use std::fmt::Write;
|
||||
|
||||
writeln!(
|
||||
out,
|
||||
"# Grocery list - {}",
|
||||
chrono::Local::now().format("%Y-%m-%d").to_string()
|
||||
)?;
|
||||
for ingredient in ingredients {
|
||||
let ingredient = ingredient.0.as_str();
|
||||
writeln!(out, "* {}", ingredient)?;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
use {
|
||||
crate::api_client::ApiClient,
|
||||
crate::commands::import,
|
||||
crate::constants,
|
||||
crate::event::{Event, Meal},
|
||||
crate::recipe,
|
||||
crate::{constants, helpers},
|
||||
anyhow::{bail, Result},
|
||||
chrono::naive::NaiveDate,
|
||||
futures::future::try_join_all,
|
||||
|
@ -110,9 +110,6 @@ async fn publish_events<'a, EventsIter>(
|
|||
where
|
||||
EventsIter: Iterator<Item = Event>,
|
||||
{
|
||||
let calendar_prototype: ics::ICalendar =
|
||||
ics::ICalendar::new("2.0", constants::CALENDAR_PROVIDER);
|
||||
|
||||
let dav_base = api_client
|
||||
.rest()
|
||||
.head(api_client.base_url().join("/.well-known/caldav")?)
|
||||
|
@ -125,18 +122,37 @@ where
|
|||
calendar.to_lowercase().as_str().replace(" ", "-")
|
||||
))?;
|
||||
|
||||
let calendar_prototype = &calendar_prototype;
|
||||
let calendar_url = &calendar_url;
|
||||
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(ev.into());
|
||||
let alarm_text_repr = format!(
|
||||
"BEGIN:VALARM\nACTION:DISPLAY\nTRIGGER:-PT15M\nDESCRIPTION:{}\nEND:VALARM",
|
||||
&helpers::ical_escape_text(&ev.recipe.name)
|
||||
);
|
||||
|
||||
let cal = icalendar::Calendar::new()
|
||||
.push::<icalendar::Event>(ev.into())
|
||||
.done();
|
||||
|
||||
let cal_as_string = (&cal.to_string())
|
||||
.replacen(
|
||||
// need to hack around inability to set PRODID in icalendar::Calendar
|
||||
"PRODID:ICALENDAR-RS",
|
||||
&format!("PRODID:{}", constants::CALENDAR_PROVIDER),
|
||||
1,
|
||||
)
|
||||
.replacen(
|
||||
// need to hack around inability to set VALARM in icalendar::Event
|
||||
"END:VEVENT",
|
||||
&format!("{}\nEND:VEVENT", alarm_text_repr),
|
||||
1,
|
||||
);
|
||||
|
||||
api_client
|
||||
.rest()
|
||||
.put(url)
|
||||
.header("Content-Type", "text/calendar; charset=utf-8")
|
||||
.body(cal.to_string())
|
||||
.body(cal_as_string)
|
||||
.send()
|
||||
.await
|
||||
});
|
||||
|
|
|
@ -9,5 +9,4 @@ pub const CALENDAR_PROVIDER: &str = concat!(
|
|||
env!("CARGO_PKG_VERSION"),
|
||||
"//EN"
|
||||
);
|
||||
pub const ICAL_LOCALTIME_FMT: &str = "%Y%m%dT%H%M%S";
|
||||
pub const ICAL_UTCTIME_FMT: &str = "%Y%m%dT%H%M%SZ";
|
||||
|
|
68
src/event.rs
68
src/event.rs
|
@ -2,12 +2,9 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use {
|
||||
crate::constants,
|
||||
crate::recipe::Recipe,
|
||||
chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc},
|
||||
ics::escape_text,
|
||||
ics::properties as calprop,
|
||||
ics::Event as CalEvent,
|
||||
chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Utc},
|
||||
icalendar::Event as CalEvent,
|
||||
std::rc::Rc,
|
||||
};
|
||||
|
||||
|
@ -47,55 +44,34 @@ impl Event {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Event> for CalEvent<'a> {
|
||||
impl From<Event> for CalEvent {
|
||||
fn from(ev: Event) -> Self {
|
||||
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();
|
||||
use icalendar::Component;
|
||||
|
||||
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(escape_text(ev.recipe.url.clone())));
|
||||
|
||||
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
|
||||
const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
||||
event.push(calprop::RRule::new(format!(
|
||||
let start_time = ev.ends_at - ev.recipe.total_time();
|
||||
|
||||
let cal_event = CalEvent::new()
|
||||
.uid(&ev.uid)
|
||||
.summary(&ev.recipe.name)
|
||||
.description(&format!("cookbook@{}", ev.recipe.id))
|
||||
.location(&ev.recipe.url)
|
||||
.timestamp(Utc::now())
|
||||
.starts(start_time)
|
||||
.ends(ev.ends_at)
|
||||
.add_property(
|
||||
// TODO make configurable yearly repetition, for now this suits my personal uses
|
||||
"RRULE",
|
||||
&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(),
|
||||
)));
|
||||
),
|
||||
)
|
||||
.done();
|
||||
|
||||
let mut trigger = calprop::Trigger::new("-PT15M");
|
||||
trigger.append(ics::parameters!("RELATED" => "START"));
|
||||
let alarm = ics::Alarm::display(
|
||||
trigger,
|
||||
calprop::Description::new(escape_text(ev.recipe.name.clone())),
|
||||
);
|
||||
event.add_alarm(alarm);
|
||||
event
|
||||
cal_event
|
||||
}
|
||||
}
|
||||
|
||||
fn dt_fmt(datetime: &DateTime<Local>) -> String {
|
||||
datetime.format(constants::ICAL_LOCALTIME_FMT).to_string()
|
||||
}
|
||||
|
||||
fn dt_utc_fmt(datetime: &DateTime<Utc>) -> String {
|
||||
datetime.format(constants::ICAL_UTCTIME_FMT).to_string()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub fn ical_escape_text(text: &str) -> String {
|
||||
/* https://www.kanzaki.com/docs/ical/text.html
|
||||
|
||||
The "TEXT" property values may also contain special characters that
|
||||
are used to signify delimiters, such as a COMMA character for lists of
|
||||
values or a SEMICOLON character for structured values. In order to
|
||||
support the inclusion of these special characters in "TEXT" property
|
||||
values, they MUST be escaped with a BACKSLASH character. A BACKSLASH
|
||||
character (US-ASCII decimal 92) in a "TEXT" property value MUST be
|
||||
escaped with another BACKSLASH character. A COMMA character in a "TEXT"
|
||||
property value MUST be escaped with a BACKSLASH character
|
||||
(US-ASCII decimal 92). A SEMICOLON character in a "TEXT" property
|
||||
value MUST be escaped with a BACKSLASH character (US-ASCII decimal
|
||||
92). However, a COLON character in a "TEXT" property value SHALL
|
||||
NOT be escaped with a BACKSLASH character.
|
||||
*/
|
||||
|
||||
let mut out = Vec::<u8>::with_capacity(text.len());
|
||||
for c in text.as_bytes() {
|
||||
match c {
|
||||
b'\\' => out.extend_from_slice(&[b'\\', b'\\']),
|
||||
b',' => out.extend_from_slice(&[b'\\', b',']),
|
||||
b';' => out.extend_from_slice(&[b'\\', b';']),
|
||||
_ => out.push(*c),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { String::from_utf8_unchecked(out) }
|
||||
}
|
|
@ -6,6 +6,7 @@ mod commands;
|
|||
mod config;
|
||||
mod constants;
|
||||
mod event;
|
||||
mod helpers;
|
||||
mod recipe;
|
||||
|
||||
use {
|
||||
|
|
|
@ -60,17 +60,17 @@ pub struct Recipe {
|
|||
//pub nutrition: Nutrition,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, Ord, Eq, PartialEq, PartialOrd)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Ingredient(String);
|
||||
pub struct Ingredient(pub String);
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tool(String);
|
||||
pub struct Tool(pub String);
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Instruction(String);
|
||||
pub struct Instruction(pub String);
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
Loading…
Reference in New Issue