2019-09-12 07:53:43 +00:00
|
|
|
use chrono::prelude::*;
|
2020-05-04 15:14:20 +00:00
|
|
|
use futures_util::stream::StreamExt;
|
2021-04-23 08:47:38 +00:00
|
|
|
use log::debug;
|
2020-05-22 16:52:58 +00:00
|
|
|
use mime::Mime;
|
2020-05-04 15:14:20 +00:00
|
|
|
use once_cell::sync::Lazy;
|
|
|
|
use reqwest::Client;
|
|
|
|
use serde_derive::Deserialize;
|
|
|
|
use serde_json::json;
|
2020-05-30 20:23:03 +00:00
|
|
|
use telegram_bot::{MessageId, types::{EditMessageText, InlineKeyboardButton, InlineKeyboardMarkup, SendMessage}, Update, UpdateKind, MessageKind, CanReplySendMessage, GetFile};
|
|
|
|
use telegram_bot::types::refs::ToMessageId;
|
2020-05-04 15:14:20 +00:00
|
|
|
use tokio::task;
|
2019-09-12 07:53:43 +00:00
|
|
|
|
2020-05-04 15:14:20 +00:00
|
|
|
use std::time::Duration;
|
2019-09-12 07:53:43 +00:00
|
|
|
|
2020-05-30 20:23:03 +00:00
|
|
|
use telegram_notes_bot::*;
|
2020-05-04 15:14:20 +00:00
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
async fn main() -> Result<(), Error> {
|
2021-04-23 08:47:38 +00:00
|
|
|
env_logger::init();
|
2020-05-24 21:10:20 +00:00
|
|
|
Lazy::force(&OWNER);
|
|
|
|
Lazy::force(&API);
|
2020-05-30 20:23:03 +00:00
|
|
|
Lazy::force(&TRILIUM_TOKEN);
|
2019-09-12 07:53:43 +00:00
|
|
|
println!("Init done!");
|
|
|
|
|
2020-05-30 20:23:03 +00:00
|
|
|
task::spawn(task_alerts());
|
|
|
|
task::spawn(event_alerts());
|
2020-05-23 15:23:36 +00:00
|
|
|
|
2020-05-30 20:23:03 +00:00
|
|
|
let mut context = Context::default();
|
2020-05-04 15:14:20 +00:00
|
|
|
|
|
|
|
let mut stream = API.stream();
|
2019-09-12 07:53:43 +00:00
|
|
|
while let Some(update) = stream.next().await {
|
2020-05-04 15:14:20 +00:00
|
|
|
if update.is_err() {
|
2020-05-04 15:36:19 +00:00
|
|
|
println!("Telegram error: {:?}", update.err().unwrap());
|
2020-05-04 15:14:20 +00:00
|
|
|
continue;
|
|
|
|
}
|
2019-09-12 07:53:43 +00:00
|
|
|
|
2020-05-30 20:23:03 +00:00
|
|
|
if let Err(e) = process_one(update.unwrap(), &mut context).await {
|
2020-05-04 15:36:19 +00:00
|
|
|
println!("Error: {}", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-05-30 20:23:03 +00:00
|
|
|
struct Context {
|
|
|
|
reminder_msg: MessageId,
|
|
|
|
reminder_text: String,
|
|
|
|
reminder_start: DateTime<Local>,
|
|
|
|
reminder_time: chrono::Duration,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Context {
|
|
|
|
fn default() -> Self {
|
|
|
|
Context {
|
|
|
|
reminder_msg: MessageId::new(1),
|
|
|
|
reminder_text: String::new(),
|
|
|
|
reminder_start: Local::now(),
|
|
|
|
reminder_time: chrono::Duration::minutes(0),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn process_one(update: Update, context: &mut Context) -> Result<(), Error> {
|
|
|
|
let reminder_msg = &mut context.reminder_msg;
|
|
|
|
let reminder_text = &mut context.reminder_text;
|
|
|
|
let reminder_start = &mut context.reminder_start;
|
|
|
|
let reminder_time = &mut context.reminder_time;
|
|
|
|
|
2020-05-04 15:36:19 +00:00
|
|
|
if let UpdateKind::Message(message) = update.kind {
|
|
|
|
let now = Local::now();
|
|
|
|
println!("[{}-{:02}-{:02} {:02}:{:02}] Receiving msg {:?}", now.year(), now.month(), now.day(), now.hour(), now.minute(), message);
|
|
|
|
if message.from.id != *OWNER {
|
2021-07-06 10:17:41 +00:00
|
|
|
println!("ignoring, not sent by authorized user");
|
2020-05-04 15:36:19 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
2020-05-30 20:23:03 +00:00
|
|
|
|
2020-05-04 15:36:19 +00:00
|
|
|
if let MessageKind::Text { ref data, .. } = message.kind {
|
2021-06-22 07:10:56 +00:00
|
|
|
if data == "/next" {
|
|
|
|
command_next().await?;
|
|
|
|
return Ok(());
|
|
|
|
} else if data == "/remindme" {
|
2020-05-04 15:36:19 +00:00
|
|
|
let mut msg = SendMessage::new(*OWNER, "in 0m: new reminder");
|
|
|
|
msg.reply_markup(get_keyboard());
|
|
|
|
*reminder_msg = API.send(msg).await?.to_message_id();
|
|
|
|
*reminder_text = "new reminder".to_owned();
|
|
|
|
*reminder_time = chrono::Duration::minutes(0);
|
|
|
|
*reminder_start = Local::now();
|
|
|
|
return Ok(());
|
|
|
|
} else if !reminder_text.is_empty() {
|
2020-06-14 16:50:03 +00:00
|
|
|
if data.starts_with("time ") && data.len() > 5 {
|
|
|
|
let time = parse_time(&data[5..]);
|
|
|
|
match time {
|
|
|
|
Ok(time) => {
|
|
|
|
*reminder_start = time;
|
|
|
|
send_message(format!("got time {}", reminder_start.format("%Y-%m-%d %H:%M"))).await
|
|
|
|
},
|
|
|
|
Err(e) => send_message(format!("{:?}", e)).await,
|
|
|
|
}?;
|
|
|
|
return Ok(());
|
|
|
|
} else {
|
|
|
|
*reminder_text = data.to_owned();
|
|
|
|
let mut edit = EditMessageText::new(*OWNER, *reminder_msg, format!("in {}: {}", format_time(*reminder_time), reminder_text));
|
|
|
|
edit.reply_markup(get_keyboard());
|
|
|
|
API.send(edit).await?;
|
|
|
|
return Ok(());
|
|
|
|
}
|
2019-09-12 07:53:43 +00:00
|
|
|
}
|
2020-05-04 15:36:19 +00:00
|
|
|
let is_url = false; //Url::parse(&data).is_ok(); // TODO: read this data from the Telegram json data (utf16 idxes..)
|
|
|
|
let formatted_text = if is_url {
|
|
|
|
format!("<ul><li><a href=\"{}\">{}</a></li></ul>", data, data)
|
|
|
|
} else {
|
|
|
|
format!("<ul><li>{}</li></ul>", data)
|
|
|
|
};
|
|
|
|
let title = format!("{} found at {:02}:{:02}", if is_url { "URL" } else { "content" }, now.hour(), now.minute());
|
|
|
|
create_text_note(&CLIENT, &*TRILIUM_TOKEN,
|
|
|
|
&title,
|
|
|
|
&formatted_text
|
|
|
|
).await?;
|
|
|
|
|
|
|
|
// answer message
|
|
|
|
if is_url {
|
|
|
|
API.send(message.text_reply("URL saved :-)")).await?;
|
|
|
|
} else {
|
|
|
|
API.send(message.text_reply("Text saved :-)")).await?;
|
2020-05-04 15:14:20 +00:00
|
|
|
}
|
2020-05-22 16:52:58 +00:00
|
|
|
} else if let MessageKind::Document { ref data, ref caption, .. } = message.kind {
|
|
|
|
let document = data;
|
|
|
|
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
|
2020-05-23 15:04:00 +00:00
|
|
|
//send_message(&text).await?;
|
2020-05-22 16:52:58 +00:00
|
|
|
let calendar = ical_parsing::parse_calendar(&text)?;
|
2020-05-23 15:04:00 +00:00
|
|
|
//send_message(format!("{:?}", calendar)).await?;
|
|
|
|
if calendar.events.len() != 1 {
|
|
|
|
return Ok(());
|
|
|
|
}
|
2020-06-09 14:20:05 +00:00
|
|
|
if CLIENT.get(&trilium_url("/custom/new_event")).form(&json!({
|
2020-05-24 21:12:00 +00:00
|
|
|
"uid": calendar.events[0].uid,
|
2020-05-23 15:04:00 +00:00
|
|
|
"name": calendar.events[0].summary,
|
2021-08-24 12:46:28 +00:00
|
|
|
"summary": calendar.events[0].description.replace("\\n", "\n"),
|
|
|
|
"summaryHtml": calendar.events[0].description_html.as_deref().map(|x| x.replace("\\n", "\n")).unwrap_or_default(),
|
2020-05-23 15:04:00 +00:00
|
|
|
"fileName": document.file_name,
|
|
|
|
"fileData": text,
|
|
|
|
"location": calendar.events[0].location,
|
|
|
|
"startTime": calendar.events[0].start.format("%Y-%m-%dT%H:%M:%S").to_string(),
|
|
|
|
"endTime": calendar.events[0].end.format("%Y-%m-%dT%H:%M:%S").to_string(),
|
|
|
|
})).send().await?.status().is_success() {
|
|
|
|
send_message("Event saved :-)").await?;
|
|
|
|
} else {
|
|
|
|
send_message("error saving event").await?;
|
|
|
|
}
|
2020-05-22 16:52:58 +00:00
|
|
|
},
|
2020-05-23 15:04:00 +00:00
|
|
|
_ => {
|
|
|
|
send_message(format!("Document {:?} {:?} {:?} {:?}", caption, document.file_id, document.file_name, document.mime_type)).await?;
|
|
|
|
}
|
2020-05-22 16:52:58 +00:00
|
|
|
}
|
2019-09-12 07:53:43 +00:00
|
|
|
}
|
2020-05-04 15:36:19 +00:00
|
|
|
} else if let UpdateKind::CallbackQuery(cb) = update.kind {
|
|
|
|
match &*cb.data.unwrap_or_default() {
|
|
|
|
"10m_cb" => {
|
|
|
|
*reminder_time = reminder_time.checked_add(&chrono::Duration::minutes(10)).unwrap();
|
|
|
|
let mut edit = EditMessageText::new(*OWNER, *reminder_msg, format!("in {}: {}", format_time(*reminder_time), reminder_text));
|
|
|
|
edit.reply_markup(get_keyboard());
|
|
|
|
API.send(edit).await?;
|
|
|
|
},
|
|
|
|
"1h_cb" => {
|
|
|
|
*reminder_time = reminder_time.checked_add(&chrono::Duration::hours(1)).unwrap();
|
|
|
|
let mut edit = EditMessageText::new(*OWNER, *reminder_msg, format!("in {}: {}", format_time(*reminder_time), reminder_text));
|
|
|
|
edit.reply_markup(get_keyboard());
|
|
|
|
API.send(edit).await?;
|
|
|
|
},
|
|
|
|
"1d_cb" => {
|
|
|
|
*reminder_time = reminder_time.checked_add(&chrono::Duration::days(1)).unwrap();
|
|
|
|
let mut edit = EditMessageText::new(*OWNER, *reminder_msg, format!("in {}: {}", format_time(*reminder_time), reminder_text));
|
|
|
|
edit.reply_markup(get_keyboard());
|
|
|
|
API.send(edit).await?;
|
|
|
|
},
|
|
|
|
"1w_cb" => {
|
|
|
|
*reminder_time = reminder_time.checked_add(&chrono::Duration::days(7)).unwrap();
|
|
|
|
let mut edit = EditMessageText::new(*OWNER, *reminder_msg, format!("in {}: {}", format_time(*reminder_time), reminder_text));
|
|
|
|
edit.reply_markup(get_keyboard());
|
|
|
|
API.send(edit).await?;
|
|
|
|
},
|
|
|
|
"save_cb" => {
|
|
|
|
let remind_time = *reminder_start + *reminder_time;
|
2020-06-09 14:20:05 +00:00
|
|
|
CLIENT.get(&trilium_url("/custom/new_reminder")).form(&json!({
|
2020-05-04 15:36:19 +00:00
|
|
|
"time": remind_time.to_rfc3339(),
|
|
|
|
"task": *reminder_text
|
2020-05-23 15:04:00 +00:00
|
|
|
})).send().await?;
|
2020-05-29 16:40:21 +00:00
|
|
|
API.send(SendMessage::new(*OWNER, format!("Reminder scheduled for {} :-)", remind_time.format("%Y-%m-%d %H:%M")))).await?;
|
2020-05-04 15:36:19 +00:00
|
|
|
*reminder_text = String::new();
|
|
|
|
},
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
println!("{:?}", update.kind);
|
2019-09-12 07:53:43 +00:00
|
|
|
}
|
2020-05-04 15:36:19 +00:00
|
|
|
Ok(())
|
2019-09-12 07:53:43 +00:00
|
|
|
}
|
|
|
|
|
2020-05-04 15:14:20 +00:00
|
|
|
fn get_keyboard() -> InlineKeyboardMarkup {
|
|
|
|
let mut keyboard = InlineKeyboardMarkup::new();
|
|
|
|
let key = InlineKeyboardButton::callback("10m", "10m_cb");
|
|
|
|
let key2 = InlineKeyboardButton::callback("1h", "1h_cb");
|
|
|
|
let key3 = InlineKeyboardButton::callback("1d", "1d_cb");
|
|
|
|
let key4 = InlineKeyboardButton::callback("1w", "1w_cb");
|
|
|
|
keyboard.add_row(vec![key, key2, key3, key4]);
|
|
|
|
let key = InlineKeyboardButton::callback("save", "save_cb");
|
|
|
|
keyboard.add_row(vec![key]);
|
|
|
|
keyboard
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn create_text_note(client: &Client, trilium_token: &str, title: &str, content: &str) -> Result<(), Error> {
|
2019-09-12 07:53:43 +00:00
|
|
|
// creating a note:
|
2020-06-09 14:20:05 +00:00
|
|
|
// curl /api/clipper/notes
|
2019-09-12 07:53:43 +00:00
|
|
|
// -H 'Accept: */*' -H 'Accept-Language: en' --compressed -H 'Content-Type: application/json'
|
|
|
|
// -H 'Authorization: icB3xohFDpkVt7YFpbTflUYC8pucmryVGpb1DFpd6ns='
|
2020-05-30 19:10:11 +00:00
|
|
|
// -H 'trilium-local-now-datetime: 2020-05-29 __:__:__.xxx+__:__'
|
2019-09-12 07:53:43 +00:00
|
|
|
// -H 'Origin: moz-extension://13bc3fd7-5cb0-4d48-b368-76e389fd7c5f'
|
|
|
|
// --data '{"title":"line 1","content":"<p>line 2</p><p>line 3</p>","clipType":"note"}'
|
2020-05-30 19:10:11 +00:00
|
|
|
let now = Local::now();
|
2020-06-09 14:20:05 +00:00
|
|
|
client.post(&trilium_url("/api/clipper/notes"))
|
2019-09-12 07:53:43 +00:00
|
|
|
.header("Authorization", trilium_token)
|
2020-05-30 19:10:11 +00:00
|
|
|
.header("trilium-local-now-datetime", now.format("%Y-%m-%d %H:%M:%S%.3f%:z").to_string())
|
2020-05-30 20:23:03 +00:00
|
|
|
.json(&json!({ "title": title, "content": content, "clipType": "note" }))
|
2019-09-12 07:53:43 +00:00
|
|
|
.send().await?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-06-22 07:10:56 +00:00
|
|
|
async fn command_next() -> Result<(), Error> {
|
|
|
|
let events = request_event_alerts().await?;
|
|
|
|
let tasks = request_task_alerts().await?;
|
|
|
|
let mut all: Vec<_> = events.into_iter().map(EventOrTask::Event).chain(tasks.into_iter().map(EventOrTask::Task)).collect();
|
|
|
|
all.sort_by_key(|x| x.time());
|
|
|
|
let mut printed = 0;
|
|
|
|
let now = Local::now();
|
2021-07-06 10:17:41 +00:00
|
|
|
let mut buf = "```\n".to_owned();
|
2021-06-22 07:10:56 +00:00
|
|
|
for x in all {
|
|
|
|
let time = x.time();
|
|
|
|
if time < now {
|
|
|
|
continue;
|
|
|
|
}
|
2021-07-06 10:17:41 +00:00
|
|
|
buf += &format!("{} {} {}\n", weekday_to_name(time.weekday()), time.format("%Y-%m-%d %H:%M").to_string(), x.description());
|
2021-06-22 07:10:56 +00:00
|
|
|
printed += 1;
|
|
|
|
if printed >= 10 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-07-06 10:17:41 +00:00
|
|
|
buf += "```\n";
|
2021-08-07 13:06:26 +00:00
|
|
|
send_message_markdown(buf).await?;
|
2021-06-22 07:10:56 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-07-06 10:17:41 +00:00
|
|
|
fn weekday_to_name(wd: Weekday) -> &'static str {
|
|
|
|
match wd {
|
|
|
|
Weekday::Mon => "Mo",
|
|
|
|
Weekday::Tue => "Di",
|
|
|
|
Weekday::Wed => "Mi",
|
|
|
|
Weekday::Thu => "Do",
|
|
|
|
Weekday::Fri => "Fr",
|
|
|
|
Weekday::Sat => "Sa",
|
|
|
|
Weekday::Sun => "So",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-22 07:10:56 +00:00
|
|
|
enum EventOrTask {
|
|
|
|
Event(UsefulEvent),
|
|
|
|
Task(UsefulTask)
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EventOrTask {
|
|
|
|
fn time(&self) -> DateTime<Local> {
|
|
|
|
match self {
|
|
|
|
EventOrTask::Event(e) => e.todo_time,
|
|
|
|
EventOrTask::Task(t) => t.todo_time,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
|
|
match self {
|
|
|
|
EventOrTask::Event(e) => &e.event.name,
|
|
|
|
EventOrTask::Task(t) => &t.task.title,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-12 07:53:43 +00:00
|
|
|
// image note:
|
2020-06-09 14:20:05 +00:00
|
|
|
// curl /api/clipper/clippings -H 'Accept: */*' -H 'Accept-Language: en' --compressed -H 'Content-Type: application/json' -H 'Authorization: icB3xohFDpkVt7YFpbTflUYC8pucmryVGpb1DFpd6ns=' -H 'Origin: moz-extension://13bc3fd7-5cb0-4d48-b368-76e389fd7c5f' --data $'{"title":"trilium/clipper.js at master \xb7 zadam/trilium","content":"<img src=\\"BoCpsLz9je8a01MdGbj4\\">","images":[{"imageId":"BoCpsLz9je8a01MdGbj4","src":"inline.png","dataUrl":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASkAAAESCAYAAAChJCPsAAAgAElEQV"}]}'
|
2020-05-04 15:14:20 +00:00
|
|
|
|
2020-05-23 15:23:36 +00:00
|
|
|
async fn event_alerts() {
|
|
|
|
loop {
|
|
|
|
let last_min = Local::now().minute();
|
|
|
|
if let Err(e) = event_alerts_soon().await {
|
|
|
|
println!("error: {}", e);
|
|
|
|
}
|
|
|
|
while Local::now().minute() == last_min {
|
2021-02-27 09:08:40 +00:00
|
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
2020-05-23 15:23:36 +00:00
|
|
|
}
|
2021-02-27 09:08:40 +00:00
|
|
|
tokio::time::sleep(Duration::from_secs(16)).await;
|
2020-05-23 15:23:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-22 07:10:56 +00:00
|
|
|
async fn request_event_alerts() -> Result<Vec<UsefulEvent>, Error> {
|
2021-02-27 09:01:05 +00:00
|
|
|
let text = CLIENT.get(&trilium_url("/custom/event_alerts")).send().await?.text().await?;
|
2021-04-23 08:47:38 +00:00
|
|
|
debug!("event_alerts response {}", text);
|
2021-02-27 09:01:05 +00:00
|
|
|
let events: Result<Vec<Event>, _> = serde_json::from_str(&text);
|
|
|
|
if events.is_err() {
|
|
|
|
eprintln!("failed to parse {}", text);
|
|
|
|
}
|
2021-06-22 07:10:56 +00:00
|
|
|
let events = events.map(|x| x.into_iter().flat_map(|event| {
|
|
|
|
let todo_time: DateTime<Local> = TimeZone::from_local_datetime(&Local,
|
|
|
|
&NaiveDateTime::parse_from_str(&event.start_time, "%Y-%m-%dT%H:%M:%S").ok()?).unwrap();
|
|
|
|
Some(UsefulEvent {
|
|
|
|
event,
|
|
|
|
todo_time
|
|
|
|
})
|
|
|
|
}).collect());
|
|
|
|
Ok(events?)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn event_alerts_soon() -> Result<(), Error> {
|
|
|
|
let now = Local::now();
|
|
|
|
|
|
|
|
let events = request_event_alerts().await?;
|
2021-04-23 08:47:38 +00:00
|
|
|
debug!("events_alerts: {} objects", events.len());
|
2020-05-23 15:23:36 +00:00
|
|
|
for event in events {
|
2021-06-22 07:10:56 +00:00
|
|
|
if event.todo_time <= now {
|
2020-05-23 15:23:36 +00:00
|
|
|
continue;
|
|
|
|
}
|
2021-06-22 07:10:56 +00:00
|
|
|
let diff = event.todo_time - now;
|
2020-05-23 15:23:36 +00:00
|
|
|
let minutes = diff.num_minutes();
|
|
|
|
if minutes == 7 * 24 * 60 || minutes == 48 * 60 || minutes == 24 * 60 || minutes == 60 || minutes == 10 {
|
2021-06-22 07:10:56 +00:00
|
|
|
event_alert_notify(&format_time(diff), event.event).await?;
|
2020-05-23 15:23:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn event_alert_notify(time_left: &str, event: Event) -> Result<(), Error> {
|
|
|
|
API.send(SendMessage::new(*OWNER, format!("{}: {}", time_left, event.name))).await?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
struct Event {
|
|
|
|
name: String,
|
|
|
|
start_time: String,
|
|
|
|
}
|
|
|
|
|
2021-06-22 07:10:56 +00:00
|
|
|
struct UsefulEvent {
|
|
|
|
event: Event,
|
|
|
|
todo_time: DateTime<Local>,
|
|
|
|
}
|
|
|
|
|
2020-05-30 20:23:03 +00:00
|
|
|
async fn task_alerts() {
|
2020-05-04 15:14:20 +00:00
|
|
|
loop {
|
|
|
|
let last_min = Local::now().minute();
|
2020-05-30 20:23:03 +00:00
|
|
|
if let Err(e) = task_alerts_soon().await {
|
2020-05-04 15:14:20 +00:00
|
|
|
println!("error: {}", e);
|
|
|
|
}
|
|
|
|
while Local::now().minute() == last_min {
|
2021-02-27 09:08:40 +00:00
|
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
2020-05-04 15:14:20 +00:00
|
|
|
}
|
2021-02-27 09:08:40 +00:00
|
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
2020-05-04 15:14:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-22 07:10:56 +00:00
|
|
|
async fn request_task_alerts() -> Result<Vec<UsefulTask>, Error> {
|
2021-02-27 09:01:05 +00:00
|
|
|
let text = CLIENT.get(&trilium_url("/custom/task_alerts")).send().await?.text().await?;
|
2021-04-23 08:47:38 +00:00
|
|
|
debug!("task_alerts response {}", text);
|
2021-02-27 09:01:05 +00:00
|
|
|
let tasks: Result<Vec<Task>, _> = serde_json::from_str(&text);
|
|
|
|
if tasks.is_err() {
|
|
|
|
eprintln!("failed to parse {}", text);
|
|
|
|
}
|
2021-06-22 07:10:56 +00:00
|
|
|
let tasks = tasks.map(|tasks| tasks.into_iter().flat_map(|task| {
|
2020-05-04 15:14:20 +00:00
|
|
|
let mut todo_date = None;
|
|
|
|
let mut todo_time = None;
|
|
|
|
let mut is_reminder = false;
|
|
|
|
for attribute in &task.attributes {
|
|
|
|
if attribute.r#type != "label" {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
match &*attribute.name {
|
|
|
|
"todoDate" => todo_date = Some(attribute.value.as_str().unwrap().to_owned()),
|
|
|
|
"todoTime" => todo_time = Some(attribute.value.as_str().unwrap().to_owned()),
|
2021-06-22 07:10:56 +00:00
|
|
|
"doneDate" => return None,
|
2020-05-04 15:14:20 +00:00
|
|
|
"reminder" => is_reminder = true,
|
2021-06-22 07:10:56 +00:00
|
|
|
"canceled" => if attribute.value.as_str().unwrap() == "true" { return None },
|
2020-05-04 15:14:20 +00:00
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if todo_date.is_none() {
|
2021-06-22 07:10:56 +00:00
|
|
|
return None;
|
2020-05-04 15:14:20 +00:00
|
|
|
}
|
|
|
|
let todo_date = todo_date.unwrap();
|
|
|
|
let parts = todo_date.split('-').collect::<Vec<_>>();
|
|
|
|
let (year, month, day) = (parts[0].parse().unwrap(), parts[1].parse().unwrap(), parts[2].parse().unwrap());
|
|
|
|
let (hour, minute, second) = if let Some(todo_time) = todo_time {
|
|
|
|
let parts = todo_time.split(':').collect::<Vec<_>>();
|
|
|
|
(parts.get(0).map(|x| x.parse().unwrap()).unwrap_or(0), parts.get(1).map(|x| x.parse().unwrap()).unwrap_or(0), parts.get(2).map(|x| x.parse().unwrap()).unwrap_or(0))
|
|
|
|
} else { (0, 0, 0) };
|
|
|
|
let todo_time: DateTime<Local> = TimeZone::from_local_datetime(&Local, &NaiveDate::from_ymd(year, month, day).and_hms(hour, minute, second)).unwrap();
|
2021-06-22 07:10:56 +00:00
|
|
|
Some(UsefulTask {
|
|
|
|
task,
|
|
|
|
todo_time,
|
|
|
|
is_reminder
|
|
|
|
})
|
|
|
|
|
|
|
|
}).collect());
|
|
|
|
Ok(tasks?)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn task_alerts_soon() -> Result<(), Error> {
|
|
|
|
let now = Local::now();
|
|
|
|
|
|
|
|
let tasks = request_task_alerts().await?;
|
|
|
|
debug!("task_alerts: {} objects", tasks.len());
|
|
|
|
for task in tasks {
|
|
|
|
if task.todo_time <= now {
|
2020-05-04 15:14:20 +00:00
|
|
|
continue;
|
|
|
|
}
|
2021-06-22 07:10:56 +00:00
|
|
|
let diff = task.todo_time - now;
|
2020-05-04 15:14:20 +00:00
|
|
|
let minutes = diff.num_minutes();
|
2021-06-22 07:10:56 +00:00
|
|
|
if !task.is_reminder && (minutes == 7 * 24 * 60 || minutes == 48 * 60 || minutes == 24 * 60 || minutes == 60 || minutes == 10) {
|
|
|
|
notify_owner(&format_time(diff), task.task).await?;
|
|
|
|
} else if task.is_reminder && minutes == 0 {
|
|
|
|
notify_owner("⏰", task.task).await?;
|
2020-05-04 15:14:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn format_time(diff: chrono::Duration) -> String {
|
|
|
|
if diff.num_weeks() > 0 {
|
|
|
|
format!("{}w", diff.num_weeks())
|
|
|
|
} else if diff.num_days() > 0 {
|
|
|
|
format!("{}d", diff.num_days())
|
|
|
|
} else if diff.num_hours() > 0 {
|
|
|
|
if diff.num_minutes() % 60 != 0 {
|
|
|
|
format!("{}h{:02}m", diff.num_hours(), diff.num_minutes() % 60)
|
|
|
|
} else {
|
|
|
|
format!("{}h", diff.num_hours())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
format!("{}m", diff.num_minutes())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-30 20:23:03 +00:00
|
|
|
async fn notify_owner(time_left: &str, task: Task) -> Result<(), Error> {
|
|
|
|
send_message(format!("{}: {}", time_left, task.title)).await
|
2020-05-22 16:52:58 +00:00
|
|
|
}
|
|
|
|
|
2020-05-04 15:14:20 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct Task {
|
|
|
|
attributes: Vec<Attribute>,
|
2020-10-16 16:40:49 +00:00
|
|
|
//contentLength: usize,
|
2020-05-04 15:14:20 +00:00
|
|
|
dateCreated: DateTime<FixedOffset>,
|
|
|
|
dateModified: DateTime<FixedOffset>,
|
|
|
|
deleteId: Option<serde_json::Value>,
|
2021-03-22 08:13:50 +00:00
|
|
|
// hash: String, // removed in 0.46
|
2020-05-04 15:14:20 +00:00
|
|
|
isContentAvailable: bool,
|
|
|
|
isDeleted: bool,
|
2021-03-22 08:13:50 +00:00
|
|
|
// isErased: i64, // removed in 0.46
|
2020-05-04 15:14:20 +00:00
|
|
|
isProtected: bool,
|
|
|
|
mime: String,
|
|
|
|
noteId: String,
|
|
|
|
title: String,
|
|
|
|
r#type: String,
|
2021-03-22 08:13:50 +00:00
|
|
|
// utcDateCreated: DateTime<Utc>, // removed in 0.46
|
2020-05-04 15:14:20 +00:00
|
|
|
utcDateModified: DateTime<Utc>,
|
|
|
|
}
|
|
|
|
|
2021-06-22 07:10:56 +00:00
|
|
|
struct UsefulTask {
|
|
|
|
task: Task,
|
|
|
|
todo_time: DateTime<Local>,
|
|
|
|
is_reminder: bool
|
|
|
|
}
|
|
|
|
|
2020-05-04 15:14:20 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct Attribute {
|
|
|
|
attributeId: String,
|
|
|
|
noteId: String,
|
|
|
|
r#type: String,
|
|
|
|
name: String,
|
|
|
|
value: serde_json::Value,
|
|
|
|
position: usize,
|
2021-03-22 08:13:50 +00:00
|
|
|
// utcDateCreated: DateTime<Utc>, // removed in 0.46
|
2020-05-04 15:14:20 +00:00
|
|
|
utcDateModified: DateTime<Utc>,
|
|
|
|
isDeleted: bool,
|
|
|
|
deleteId: Option<serde_json::Value>,
|
2021-03-22 08:13:50 +00:00
|
|
|
// hash: String, // removed in 0.46
|
2020-05-04 15:14:20 +00:00
|
|
|
isInheritable: bool,
|
2020-05-13 20:50:49 +00:00
|
|
|
//isOwned: bool, // removed in 0.42.2
|
2020-05-04 15:14:20 +00:00
|
|
|
}
|