aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGustav Sörnäs <gustav@sornas.net>2020-12-01 07:46:29 +0100
committerGustav Sörnäs <gustav@sornas.net>2020-12-01 07:46:29 +0100
commit8b8c829793c026eee5fac474399a9a7179a10c80 (patch)
tree133d1102e7da0a1c89f0bf8b027e9461e4aaef59
parentc1b9d90f2f81f7acb0f4a88acf7056874e9cbe2f (diff)
parent31f5366ef857bc3e50f7a4485250cbe50d903e0b (diff)
downloadkodapa-8b8c829793c026eee5fac474399a9a7179a10c80.tar.gz
Merge branch 'reminders' into main
-rw-r--r--Cargo.toml1
-rw-r--r--src/agenda.rs30
-rw-r--r--src/discord.rs38
-rw-r--r--src/main.rs13
-rw-r--r--src/reminder.rs106
-rw-r--r--src/slack.rs40
6 files changed, 205 insertions, 23 deletions
diff --git a/Cargo.toml b/Cargo.toml
index ee35f28..252b685 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/agenda.rs b/src/agenda.rs
index ef17f5a..1254a41 100644
--- a/src/agenda.rs
+++ b/src/agenda.rs
@@ -35,6 +35,25 @@ 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::<Vec<_>>()
+ .join("\n");
+ write!(
+ f,
+ "{}",
+ match s.as_str() {
+ "" => "Empty agenda",
+ _ => &s,
+ }
+ )
+ }
+}
+
pub enum Emoji {
Ok,
Confused,
@@ -61,16 +80,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::<Vec<_>>()
- .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..40c30ac 100644
--- a/src/discord.rs
+++ b/src/discord.rs
@@ -1,4 +1,5 @@
-use crate::agenda::{parse_message, AgendaPoint, Emoji};
+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<AgendaPoint>,
receiver: mpsc::UnboundedReceiver<AgendaPoint>,
+ reminder: watch::Receiver<ReminderType>,
) {
println!("Setting up Discord");
@@ -45,8 +47,9 @@ pub async fn handle(
.map(|id| Some(ChannelId(id.parse::<u64>().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,32 @@ async fn receive_from_slack(
}
}
}
+
+async fn handle_reminders(
+ mut reminder: watch::Receiver<ReminderType>,
+ client: Arc<Mutex<discord::Discord>>,
+ channel: Option<ChannelId>,
+) {
+ 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 47daed8..8980b31 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,20 +1,23 @@
mod agenda;
mod discord;
+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::<AgendaPoint>();
let (from_slack, to_discord) = mpsc::unbounded_channel::<AgendaPoint>();
+ 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/reminder.rs b/src/reminder.rs
new file mode 100644
index 0000000..421720c
--- /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<Local>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Reminders {
+ reminders: Vec<Reminder>,
+}
+
+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<ReminderType>) {
+ let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(1000));
+
+ 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();
+ interval.tick().await;
+ }
+}
+
+fn read_reminders() -> Reminders {
+ 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<Local>, meeting: DateTime<Local>) -> 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<Local> {
+ // 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)));
+ }
+}
diff --git a/src/slack.rs b/src/slack.rs
index 45a3dc6..0b02fac 100644
--- a/src/slack.rs
+++ b/src/slack.rs
@@ -1,4 +1,5 @@
-use crate::agenda::{parse_message, AgendaPoint, Emoji};
+use crate::agenda::{self, parse_message, AgendaPoint, Emoji};
+use crate::reminder::ReminderType;
use futures::join;
use slack::{error::Error, 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;
@@ -141,7 +142,7 @@ impl slack::EventHandler for Handler {
)
.unwrap();
}
- _ => {}
+ _ => {} // parse_message return
}
}
}
@@ -161,6 +162,7 @@ impl slack::EventHandler for Handler {
pub async fn handle(
sender: mpsc::UnboundedSender<AgendaPoint>,
receiver: mpsc::UnboundedReceiver<AgendaPoint>,
+ reminder: watch::Receiver<ReminderType>,
) {
println!("Setting up Slack");
@@ -186,7 +188,13 @@ 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 || {
loop {
match client.run(&mut handler) {
@@ -199,7 +207,6 @@ pub async fn handle(
}
}
}),
- spawn(receive_from_discord(receiver, slack_sender, channel))
);
}
@@ -220,3 +227,26 @@ async fn receive_from_discord(
}
}
}
+
+async fn handle_reminders(
+ mut reminder: watch::Receiver<ReminderType>,
+ sender: slack::Sender,
+ channel: Option<String>,
+) {
+ 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 => {}
+ }
+ }
+ }
+}