From 3e9712c3782fc9c1450738236c234c63022acd11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustav=20S=C3=B6rn=C3=A4s?= Date: Sun, 22 Nov 2020 22:57:40 +0100 Subject: implement reminders with hard-coded meeting time --- Cargo.toml | 1 + src/main.rs | 1 + src/reminder.rs | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/reminder.rs diff --git a/Cargo.toml b/Cargo.toml index 9f61b8e..57d042e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = "0.4" futures = "0.3" serde_json = "1.0" slack = "0.25" diff --git a/src/main.rs b/src/main.rs index 47daed8..6bd2de5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod agenda; mod discord; +mod reminder; mod slack; use crate::agenda::AgendaPoint; diff --git a/src/reminder.rs b/src/reminder.rs new file mode 100644 index 0000000..20e8fb8 --- /dev/null +++ b/src/reminder.rs @@ -0,0 +1,106 @@ +use chrono::{DateTime, Datelike, Duration, Local, NaiveTime, Weekday}; +use serde::{Deserialize, Serialize}; +use std::fs; +use tokio::sync::watch; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ReminderType { + // Different types of reminders are possible. + // e.g. different reminders for the day before and one hour before. + Void, + OneHour, //TODO struct instead +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Reminder { + reminder_type: ReminderType, + last_fire: DateTime, +} + +#[derive(Serialize, Deserialize)] +pub struct Reminders { + reminders: Vec, +} + +impl Reminders { + fn write(&self) { + fs::write( + std::path::Path::new("reminders.json"), + serde_json::to_string_pretty(&self).expect("Can't serialize reminders"), + ) + .expect("Can't write reminders.json") + } +} + +pub async fn handle(sender: watch::Sender) { + let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(1000)); + + let now = Local::now(); + let next = next_meeting(); + let mut reminders = read_reminders(); + for mut reminder in &mut reminders.reminders { + match reminder.reminder_type { + ReminderType::OneHour => { + if in_remind_zone(now, next) && !in_remind_zone(reminder.last_fire, next) { + sender.broadcast(ReminderType::OneHour).unwrap(); + reminder.last_fire = now; + } + }, + _ => {}, + } + } + reminders.write(); + + loop { + interval.tick().await; + } +} + +fn read_reminders() -> Reminders { + serde_json::from_str( + &fs::read_to_string("reminders.json").expect("Can't read reminders.json") + ) + .expect("Error parsing reminders.json") +} + +fn in_remind_zone(dt: DateTime, meeting: DateTime) -> bool { + // Wether we're in a "send reminder"-zone. + // Currently implemented as "are we 1 hour before?". + ((meeting - Duration::hours(1))..meeting).contains(&dt) +} + +fn next_meeting() -> DateTime { + // Check current datetime and calculate when the next meeting is. + let now = Local::now(); + let meeting_time = NaiveTime::from_hms(12, 15, 00); + let meeting = match Datelike::weekday(&now) { + Weekday::Thu => { + // same day as meeting. + // next week if meeting has occured. + let date_delta = Duration::weeks( + if now.time() < meeting_time { 0 } else { 1 } + ); + (now.date() + date_delta).and_time(meeting_time).unwrap() + }, + _ => { + let dow_index: i64 = now.date().weekday().num_days_from_monday().into(); + let date_delta = Duration::days((3 - dow_index).rem_euclid(7)); + (now.date() + date_delta).and_time(meeting_time).unwrap() + }, + }; + assert!(meeting.weekday() == Weekday::Thu); + meeting +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn in_remind_zone() { + let now = Local::now(); + assert!(super::in_remind_zone(now, now + Duration::minutes(30))); + assert!(!super::in_remind_zone(now, now + Duration::hours(2))); + assert!(!super::in_remind_zone(now, now - Duration::minutes(30))); + } +} -- cgit v1.2.1 From cfc6244b9cb88ec9521ec02dd0119c3b9f9bbee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustav=20S=C3=B6rn=C3=A4s?= Date: Sun, 22 Nov 2020 23:00:36 +0100 Subject: implement Display for Agenda --- src/agenda.rs | 26 ++++++++++++++++---------- src/discord.rs | 2 +- src/slack.rs | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/agenda.rs b/src/agenda.rs index 2543daa..a132bbb 100644 --- a/src/agenda.rs +++ b/src/agenda.rs @@ -35,6 +35,21 @@ impl Agenda { } } +impl fmt::Display for Agenda { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = self + .points + .iter() + .map(|p| p.to_string()) + .collect::>() + .join("\n"); + write!(f, "{}", match s.as_str() { + "" => "Empty agenda", + _ => &s + }) + } +} + pub enum Emoji { Ok, Confused, @@ -61,16 +76,7 @@ where agenda.write(); Some(Emoji::Ok) } else if message.starts_with("!agenda") { - let s = read_agenda() - .points - .iter() - .map(|p| p.to_string()) - .collect::>() - .join("\n"); - send_message(match s.as_str() { - "" => "Agenda is empty".to_string(), - _ => s, - }); + send_message(read_agenda().to_string()); None } else if message.starts_with("!clear") { Agenda { points: Vec::new() }.write(); diff --git a/src/discord.rs b/src/discord.rs index e0119a4..fb121fb 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,4 +1,4 @@ -use crate::agenda::{parse_message, AgendaPoint, Emoji}; +use crate::agenda::{self, parse_message, AgendaPoint, Emoji}; use discord::{ model::{ChannelId, Event, PossibleServer, ReactionEmoji, UserId}, diff --git a/src/slack.rs b/src/slack.rs index 49c8cfa..9698109 100644 --- a/src/slack.rs +++ b/src/slack.rs @@ -1,4 +1,4 @@ -use crate::agenda::{parse_message, AgendaPoint, Emoji}; +use crate::agenda::{self, parse_message, AgendaPoint, Emoji}; use futures::join; use slack::{Event, Message}; -- cgit v1.2.1 From 5788bc84c13ed56324bdc6dc7c61bb0ca12a9256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustav=20S=C3=B6rn=C3=A4s?= Date: Sun, 22 Nov 2020 23:01:28 +0100 Subject: print agenda when reminders are requested --- src/agenda.rs | 2 +- src/discord.rs | 32 ++++++++++++++++++++++++++++++-- src/main.rs | 12 +++++++----- src/slack.rs | 33 ++++++++++++++++++++++++++++----- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/agenda.rs b/src/agenda.rs index a132bbb..8ed1840 100644 --- a/src/agenda.rs +++ b/src/agenda.rs @@ -91,7 +91,7 @@ where } } -fn read_agenda() -> Agenda { +pub fn read_agenda() -> Agenda { serde_json::from_str::( &fs::read_to_string("agenda.json").expect("Can't read agenda.json"), ) diff --git a/src/discord.rs b/src/discord.rs index fb121fb..438f0ed 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,4 +1,5 @@ use crate::agenda::{self, parse_message, AgendaPoint, Emoji}; +use crate::reminder::ReminderType; use discord::{ model::{ChannelId, Event, PossibleServer, ReactionEmoji, UserId}, @@ -10,7 +11,7 @@ use std::{ sync::{Arc, Mutex}, }; use tokio::{ - sync::mpsc, + sync::{mpsc, watch}, task::{spawn, spawn_blocking}, }; @@ -29,6 +30,7 @@ struct Handler { pub async fn handle( sender: mpsc::UnboundedSender, receiver: mpsc::UnboundedReceiver, + reminder: watch::Receiver, ) { println!("Setting up Discord"); @@ -45,8 +47,9 @@ pub async fn handle( .map(|id| Some(ChannelId(id.parse::().unwrap()))) .unwrap_or(CHANNEL); - let (_, _) = join!( + let (_, _, _) = join!( spawn(receive_from_slack(receiver, Arc::clone(&client), channel)), + spawn(handle_reminders(reminder, Arc::clone(&client), channel)), spawn_blocking(move || receive_events(&mut Handler { _our_id, connection, @@ -159,3 +162,28 @@ async fn receive_from_slack( } } } + +async fn handle_reminders( + mut reminder: watch::Receiver, + client: Arc>, + channel: Option, +) { + if let Some(channel) = channel { + while let Some(reminder) = reminder.recv().await { + match reminder { + ReminderType::OneHour => { + client + .lock() + .unwrap() + .send_message(channel, + &format!("Meeting in one hour!\n{}", + agenda::read_agenda().to_string()), + "", + false) + .unwrap(); + }, + ReminderType::Void => {} + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 6bd2de5..8980b31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,18 +4,20 @@ mod reminder; mod slack; use crate::agenda::AgendaPoint; +use crate::reminder::ReminderType; use futures::join; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, watch}; #[tokio::main] async fn main() { - println!("Hello, world!"); - let (from_discord, to_slack) = mpsc::unbounded_channel::(); let (from_slack, to_discord) = mpsc::unbounded_channel::(); + let (reminder_sender, reminder_receiver) = watch::channel(ReminderType::Void); + join!( - discord::handle(from_discord, to_discord), - slack::handle(from_slack, to_slack), + reminder::handle(reminder_sender), + discord::handle(from_discord, to_discord, reminder_receiver.clone()), + slack::handle(from_slack, to_slack, reminder_receiver), ); } diff --git a/src/slack.rs b/src/slack.rs index 9698109..1481cf1 100644 --- a/src/slack.rs +++ b/src/slack.rs @@ -1,4 +1,5 @@ use crate::agenda::{self, parse_message, AgendaPoint, Emoji}; +use crate::reminder::ReminderType; use futures::join; use slack::{Event, Message}; @@ -9,7 +10,7 @@ use std::{ }; use tokio::{ runtime::Runtime, - sync::mpsc, + sync::{mpsc, watch}, task::{spawn, spawn_blocking}, }; use tokio_compat_02::FutureExt; @@ -137,11 +138,11 @@ impl slack::EventHandler for Handler { timestamp: Some(msg.ts.unwrap()), }, ) - .compat(), + .compat(), ) .unwrap(); } - _ => {} + _ => {} // parse_message return } } } @@ -161,6 +162,7 @@ impl slack::EventHandler for Handler { pub async fn handle( sender: mpsc::UnboundedSender, receiver: mpsc::UnboundedReceiver, + reminder: watch::Receiver, ) { println!("Setting up Slack"); @@ -186,14 +188,15 @@ pub async fn handle( ); let slack_sender = client.sender().clone(); - let (_, _) = join!( + let (_, _, _) = join!( + spawn(receive_from_discord(receiver, slack_sender.clone(), channel.clone())), + spawn(handle_reminders(reminder, slack_sender, channel)), spawn_blocking(move || { match client.run(&mut handler) { Ok(_) => {} Err(e) => println!("Error: {}", e), } }), - spawn(receive_from_discord(receiver, slack_sender, channel)) ); } @@ -214,3 +217,23 @@ async fn receive_from_discord( } } } + +async fn handle_reminders( + mut reminder: watch::Receiver, + sender: slack::Sender, + channel: Option, +) { + if let Some(channel) = channel { + while let Some(reminder) = reminder.recv().await { + match reminder { + ReminderType::OneHour => { + sender.send_typing(&channel).unwrap(); + sender + .send_message(&channel, &format!("Meeting in one hour!\n{}", agenda::read_agenda())) + .unwrap(); + }, + ReminderType::Void => {} + } + } + } +} -- cgit v1.2.1 From 886500a0224a646d599f6df0a637c37b4e86d034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustav=20S=C3=B6rn=C3=A4s?= Date: Sun, 22 Nov 2020 23:02:27 +0100 Subject: cargo fmt --- src/agenda.rs | 12 ++++++++---- src/discord.rs | 16 ++++++++++------ src/reminder.rs | 20 ++++++++------------ src/slack.rs | 15 +++++++++++---- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/agenda.rs b/src/agenda.rs index 8ed1840..8f71c87 100644 --- a/src/agenda.rs +++ b/src/agenda.rs @@ -43,10 +43,14 @@ impl fmt::Display for Agenda { .map(|p| p.to_string()) .collect::>() .join("\n"); - write!(f, "{}", match s.as_str() { - "" => "Empty agenda", - _ => &s - }) + write!( + f, + "{}", + match s.as_str() { + "" => "Empty agenda", + _ => &s, + } + ) } } diff --git a/src/discord.rs b/src/discord.rs index 438f0ed..40c30ac 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -175,13 +175,17 @@ async fn handle_reminders( client .lock() .unwrap() - .send_message(channel, - &format!("Meeting in one hour!\n{}", - agenda::read_agenda().to_string()), - "", - false) + .send_message( + channel, + &format!( + "Meeting in one hour!\n{}", + agenda::read_agenda().to_string() + ), + "", + false, + ) .unwrap(); - }, + } ReminderType::Void => {} } } diff --git a/src/reminder.rs b/src/reminder.rs index 20e8fb8..7b36b2d 100644 --- a/src/reminder.rs +++ b/src/reminder.rs @@ -8,7 +8,7 @@ pub enum ReminderType { // Different types of reminders are possible. // e.g. different reminders for the day before and one hour before. Void, - OneHour, //TODO struct instead + OneHour, //TODO struct instead } #[derive(Debug, Serialize, Deserialize)] @@ -45,8 +45,8 @@ pub async fn handle(sender: watch::Sender) { sender.broadcast(ReminderType::OneHour).unwrap(); reminder.last_fire = now; } - }, - _ => {}, + } + _ => {} } } reminders.write(); @@ -57,10 +57,8 @@ pub async fn handle(sender: watch::Sender) { } fn read_reminders() -> Reminders { - serde_json::from_str( - &fs::read_to_string("reminders.json").expect("Can't read reminders.json") - ) - .expect("Error parsing reminders.json") + serde_json::from_str(&fs::read_to_string("reminders.json").expect("Can't read reminders.json")) + .expect("Error parsing reminders.json") } fn in_remind_zone(dt: DateTime, meeting: DateTime) -> bool { @@ -77,16 +75,14 @@ fn next_meeting() -> DateTime { Weekday::Thu => { // same day as meeting. // next week if meeting has occured. - let date_delta = Duration::weeks( - if now.time() < meeting_time { 0 } else { 1 } - ); + let date_delta = Duration::weeks(if now.time() < meeting_time { 0 } else { 1 }); (now.date() + date_delta).and_time(meeting_time).unwrap() - }, + } _ => { let dow_index: i64 = now.date().weekday().num_days_from_monday().into(); let date_delta = Duration::days((3 - dow_index).rem_euclid(7)); (now.date() + date_delta).and_time(meeting_time).unwrap() - }, + } }; assert!(meeting.weekday() == Weekday::Thu); meeting diff --git a/src/slack.rs b/src/slack.rs index 1481cf1..2a4c155 100644 --- a/src/slack.rs +++ b/src/slack.rs @@ -138,7 +138,7 @@ impl slack::EventHandler for Handler { timestamp: Some(msg.ts.unwrap()), }, ) - .compat(), + .compat(), ) .unwrap(); } @@ -189,7 +189,11 @@ pub async fn handle( let slack_sender = client.sender().clone(); let (_, _, _) = join!( - spawn(receive_from_discord(receiver, slack_sender.clone(), channel.clone())), + spawn(receive_from_discord( + receiver, + slack_sender.clone(), + channel.clone() + )), spawn(handle_reminders(reminder, slack_sender, channel)), spawn_blocking(move || { match client.run(&mut handler) { @@ -229,9 +233,12 @@ async fn handle_reminders( ReminderType::OneHour => { sender.send_typing(&channel).unwrap(); sender - .send_message(&channel, &format!("Meeting in one hour!\n{}", agenda::read_agenda())) + .send_message( + &channel, + &format!("Meeting in one hour!\n{}", agenda::read_agenda()), + ) .unwrap(); - }, + } ReminderType::Void => {} } } -- cgit v1.2.1 From 550345fa3cd8b771005645e43deda50fbb7c2d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustav=20S=C3=B6rn=C3=A4s?= Date: Sun, 22 Nov 2020 23:10:41 +0100 Subject: create reminders.json if not exists --- src/reminder.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/reminder.rs b/src/reminder.rs index 7b36b2d..56c8d97 100644 --- a/src/reminder.rs +++ b/src/reminder.rs @@ -57,8 +57,13 @@ pub async fn handle(sender: watch::Sender) { } fn read_reminders() -> Reminders { - serde_json::from_str(&fs::read_to_string("reminders.json").expect("Can't read reminders.json")) - .expect("Error parsing reminders.json") + match fs::read_to_string("reminders.json") { + Ok(s) => serde_json::from_str(&s).expect("Error parsing reminders.json"), + Err(_) => Reminders { reminders: vec![Reminder { + reminder_type: ReminderType::OneHour, + last_fire: Local::now(), + }]}, + } } fn in_remind_zone(dt: DateTime, meeting: DateTime) -> bool { -- cgit v1.2.1 From 31f5366ef857bc3e50f7a4485250cbe50d903e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustav=20S=C3=B6rn=C3=A4s?= Date: Thu, 26 Nov 2020 11:32:10 +0100 Subject: actually tick reminders --- src/reminder.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/reminder.rs b/src/reminder.rs index 56c8d97..421720c 100644 --- a/src/reminder.rs +++ b/src/reminder.rs @@ -35,23 +35,22 @@ impl Reminders { pub async fn handle(sender: watch::Sender) { let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(1000)); - let now = Local::now(); - let next = next_meeting(); - let mut reminders = read_reminders(); - for mut reminder in &mut reminders.reminders { - match reminder.reminder_type { - ReminderType::OneHour => { - if in_remind_zone(now, next) && !in_remind_zone(reminder.last_fire, next) { - sender.broadcast(ReminderType::OneHour).unwrap(); - reminder.last_fire = now; + loop { + let now = Local::now(); + let next = next_meeting(); + let mut reminders = read_reminders(); + for mut reminder in &mut reminders.reminders { + match reminder.reminder_type { + ReminderType::OneHour => { + if in_remind_zone(now, next) && !in_remind_zone(reminder.last_fire, next) { + sender.broadcast(ReminderType::OneHour).unwrap(); + reminder.last_fire = now; + } } + _ => {} } - _ => {} } - } - reminders.write(); - - loop { + reminders.write(); interval.tick().await; } } -- cgit v1.2.1