diff --git a/Cargo.lock b/Cargo.lock index 27150bf..65b201f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1129,6 +1129,7 @@ dependencies = [ "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "ical 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index f057da3..42abc37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "telegram_notes_bot" version = "0.1.0" authors = ["FliegendeWurst <2012gdwu@web.de>"] +license = "GPL-3.0+" edition = "2018" [dependencies] @@ -18,3 +19,4 @@ once_cell = "1.3.1" thiserror = "1.0.15" serde_json = "1.0.51" ical = "0.6.0" +mime = "0.3.16" diff --git a/src/ical_parsing.rs b/src/ical_parsing.rs new file mode 100644 index 0000000..8e51a1f --- /dev/null +++ b/src/ical_parsing.rs @@ -0,0 +1,116 @@ +use chrono::{Duration, NaiveDateTime, NaiveDate}; +use ical::parser::ical::component::IcalEvent; +use ical::parser::ical::IcalParser; +use thiserror::Error; + +#[derive(Debug)] +pub struct Calendar { + pub name: String, + pub events: Vec, +} + +#[derive(Debug)] +pub struct Event { + pub uid: String, + pub summary: String, + pub description: String, + pub start: NaiveDateTime, + pub end: NaiveDateTime, + pub duration: Option, + pub location: String, +} + +pub fn parse_calendar(data: &str) -> Result { + let cal = IcalParser::new(data.as_bytes()).next().ok_or(Error::Nothing)??; + let mut name = None; + let mut events = Vec::new(); + for prop in cal.properties { + match prop.name.as_ref() { + "NAME" => name = Some(prop.value.unwrap_or_default()), + _ => {} + } + } + for event in cal.events { + events.push(process_event(event)?); + } + let name = name.unwrap_or_default(); + Ok(Calendar { + name, events + }) +} + +fn process_event(event: IcalEvent) -> Result { + let mut uid = None; + let mut summary = None; + let mut description = None; + let mut start = None; + let mut end = None; + let mut duration = None; + let mut location = None; + for prop in event.properties { + let value = prop.value.unwrap_or_default(); + match prop.name.as_ref() { + "UID" => uid = Some(value), + "SUMMARY" => summary = Some(value), + "LOCATION" => location = Some(value), + "DESCRIPTION" => description = Some(value), + "STATUS" => { /* TODO: status */ }, + "DTSTART" => start = Some(process_dt(&value)?), + "DTEND" => end = Some(process_dt(&value)?), + "DURATION" => duration = Some(process_duration(&value)?), + "RRULE" => { /* TODO: periodic */ }, + _ => (), + }; + } + // TODO: don't put defaults here + let start = start.ok_or(Error::Data("no dtstart"))?; + let end = end.ok_or(Error::Data("no dtend"))?; + Ok(Event { + uid: uid.unwrap_or_default(), + summary: summary.unwrap_or_default(), + description: description.unwrap_or_default(), + start: start, + end: end, + duration, + location: location.unwrap_or_default(), + }) +} + +fn process_dt(value: &str) -> Result { + // 20200626T140000 + if value.len() != 15 { + return Err(Error::Data("invalid dt length")); + } + // TODO: error handling + let year = value[0..4].parse()?; + let month = value[4..6].parse()?; + let day = value[6..8].parse()?; + let hour = value[9..11].parse()?; + let minute = value[11..13].parse()?; + let second = value[13..15].parse()?; + + Ok(NaiveDate::from_ymd(year, month, day).and_hms(hour, minute, second)) +} + +fn process_duration(_value: &str) -> Result { + // TODO + return Err(Error::Data("duration parsing not implemented")); +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("parsing error: {0}")] + Ical(ical::parser::ParserError), + #[error("data error: {0}")] + Data(&'static str), + #[error("parse error: {0}")] + IntegerParsing(#[from] std::num::ParseIntError), + #[error("no calendar found")] + Nothing +} + +impl From for Error { + fn from(x: ical::parser::ParserError) -> Self { + Error::Ical(x) + } +} diff --git a/src/main.rs b/src/main.rs index dee0131..729817f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use chrono::prelude::*; use futures_util::stream::StreamExt; use maplit::hashmap; +use mime::Mime; use once_cell::sync::Lazy; use reqwest::Client; use serde_derive::Deserialize; @@ -16,10 +17,15 @@ use std::env; use std::sync::Arc; use std::time::Duration; +mod ical_parsing; + +static TELEGRAM_BOT_TOKEN: Lazy = Lazy::new(|| { + env::var("TELEGRAM_BOT_TOKEN").expect("TELEGRAM_BOT_TOKEN not set") +}); + static API: Lazy> = Lazy::new(|| { - let telegram_token = env::var("TELEGRAM_BOT_TOKEN").expect("TELEGRAM_BOT_TOKEN not set"); println!("Initializing Telegram API.."); - Arc::new(Api::new(telegram_token)) + Arc::new(Api::new(&*TELEGRAM_BOT_TOKEN)) }); static TRILIUM_TOKEN: Lazy = Lazy::new(|| { @@ -122,6 +128,24 @@ async fn process_one(update: Update, reminder_msg: &mut MessageId, reminder_text } else { API.send(message.text_reply("Text saved :-)")).await?; } + } else if let MessageKind::Document { ref data, ref caption, .. } = message.kind { + let document = data; + send_message(format!("Document {:?} {:?} {:?} {:?}", caption, document.file_id, document.file_name, document.mime_type)).await?; + let get_file = GetFile::new(&document); + let file = API.send(get_file).await?; + let url = file.get_url(&TELEGRAM_BOT_TOKEN).ok_or_else(|| error("url is none"))?; + let data = CLIENT.get(&url).send().await?.bytes().await?; + let mime: Mime = document.mime_type.as_ref().ok_or_else(|| error("no mime type"))?.parse()?; + match (mime.type_(), mime.subtype()) { + (mime::TEXT, x) if x == "calendar" => { + let text = String::from_utf8_lossy(&data); + let text = text.replace("\n<", "<"); // newlines in HTML values + send_message(&text).await?; + let calendar = ical_parsing::parse_calendar(&text)?; + send_message(format!("{:?}", calendar)).await?; + }, + _ => {} + } } } else if let UpdateKind::CallbackQuery(cb) = update.kind { match &*cb.data.unwrap_or_default() { @@ -278,6 +302,11 @@ async fn notify_owner_impl(time_left: &str, task: Task) -> Result<(), Error> { Ok(()) } +async fn send_message>(msg: S) -> Result<(), Error> { + API.send(SendMessage::new(*OWNER, msg.into())).await?; + Ok(()) +} + #[derive(Deserialize, Debug)] #[allow(non_snake_case)] struct Task { @@ -323,4 +352,14 @@ pub enum Error { Network(#[from] reqwest::Error), #[error("telegram error: {0}")] Telegram(#[from] telegram_bot::Error), + #[error("mime parsing error: {0}")] + Mime(#[from] mime::FromStrError), + #[error("ical parsing error: {0}")] + Ical(#[from] ical_parsing::Error), + #[error("internal error: {0}")] + CustomMessage(String), +} + +fn error>(msg: S) -> Error { + Error::CustomMessage(msg.into()) }