diff --git a/locales/de.yml b/locales/de.yml index dc77a86..d4d4fc9 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -1,4 +1,10 @@ messages: next_appointment: "Nächster Termin: %{weekday}, %{date}" appointment_today: "Heute um %{start_time} Uhr geht's weiter" + appointment_tomorrow: "Morgen um %{start_time} Uhr geht's weiter" + appointment_soon: "In %{no_days} um %{start_time} Uhr geht's weiter" starting_now: "Jetzt geht's weiter" + +errors: + param_no_days: "Bitte gib die Anzahl Tage an, die du vorab erinnert werden möchtest, als Parameter an" + param_no_days_nonnegative: "Die Anzahl der Tage darf nicht negativ sein" \ No newline at end of file diff --git a/locales/en.yml b/locales/en.yml index d2c6b28..fce363b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,4 +1,10 @@ messages: next_appointment: "Next meeting: %{weekday}, %{date} (%{uk_time} UK time)" appointment_today: "Next meeting is today at %{start_time} (%{uk_time} UK time)" + appointment_tomorrow: "Next meeting is tomorrow at %{start_time} (%{uk_time} UK time)" + appointment_soon: "Next meeting is in %{no_days} at %{start_time} (%{uk_time} UK time)" starting_now: "The meeting starts now" + +errors: + param_no_days: "Please specify the number of days that you want to be reminded in advance as parameter" + param_no_days_nonnegative: "The number of days may not be negative" \ No newline at end of file diff --git a/migrations/2024-01-11-091208_remind_days_ahead/down.sql b/migrations/2024-01-11-091208_remind_days_ahead/down.sql new file mode 100644 index 0000000..5c093eb --- /dev/null +++ b/migrations/2024-01-11-091208_remind_days_ahead/down.sql @@ -0,0 +1 @@ +ALTER TABLE chat DROP remind_days_ahead; \ No newline at end of file diff --git a/migrations/2024-01-11-091208_remind_days_ahead/up.sql b/migrations/2024-01-11-091208_remind_days_ahead/up.sql new file mode 100644 index 0000000..8ae2d8d --- /dev/null +++ b/migrations/2024-01-11-091208_remind_days_ahead/up.sql @@ -0,0 +1 @@ +ALTER TABLE chat ADD remind_days_ahead BIGINT NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index cf7a2e1..1148eac 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -11,7 +11,9 @@ use teloxide::dispatching::dialogue; use teloxide::dispatching::dialogue::InMemStorage; use teloxide::dispatching::{UpdateFilterExt, UpdateHandler}; use teloxide::dptree::{case, deps}; +use teloxide::payloads::SendMessageSetters; use teloxide::prelude::Dispatcher; +use teloxide::requests::Request; use teloxide::requests::Requester; use teloxide::types::ChatId; use teloxide::types::ChatMemberKind; @@ -32,6 +34,7 @@ pub enum Command { #[command()] SetCalendar, SetLocale, + RemindDaysAhead, } pub async fn spawn(bot: Throttle, db: Database) { @@ -45,7 +48,8 @@ pub async fn spawn(bot: Throttle, db: Database) { fn build_handler_chain() -> UpdateHandler { let command_handler = teloxide::filter_command::() .branch(case![Command::SetCalendar].endpoint(set_calendar)) - .branch(case![Command::SetLocale].endpoint(set_locale)); + .branch(case![Command::SetLocale].endpoint(set_locale)) + .branch(case![Command::RemindDaysAhead].endpoint(set_remind_days_ahead)); let my_chat_member_handler = Update::filter_my_chat_member().endpoint(handle_my_chat_member); @@ -96,6 +100,7 @@ async fn set_calendar(bot: Throttle, msg: Message, db: Database) -> Result< last_reminder: None, pinned_message_id: None, locale: "de".into(), + remind_days_ahead: 0, }; fetch_and_announce_appointment(&bot, &mut chat_info, &db).await?; @@ -123,6 +128,61 @@ async fn set_locale(_bot: Throttle, msg: Message, db: Database) -> Result<( Ok(()) } +async fn set_remind_days_ahead( + bot: Throttle, + msg: Message, + db: Database, +) -> Result<(), Error> { + let chat = db.lock().await.transaction(|db| { + use schema::chat::dsl::*; + chat.filter(telegram_id.eq(msg.chat.id.0)) + .first::(db) + })?; + + let chat = ChatInfo::::from(chat); + + let days_ahead = msg + .text() + .map(|text| text.splitn(2, " ").nth(1).map(|param| param.parse().ok())) + .flatten() + .flatten(); + + if days_ahead.is_none() { + bot.send_message( + msg.chat.id, + t!("errors.param_no_days", locale = &chat.locale), + ) + .reply_to_message_id(msg.id) + .send() + .await?; + return Ok(()); + } + + let days_ahead: i64 = days_ahead.unwrap(); + + if days_ahead < 0 { + bot.send_message( + msg.chat.id, + t!("errors.param_no_days_nonnegative", locale = &chat.locale), + ) + .reply_to_message_id(msg.id) + .send() + .await?; + return Ok(()); + } + + db.lock().await.transaction::<_, Error, _>(|db| { + use schema::chat::dsl::*; + diesel::update(chat) + .filter(telegram_id.eq(msg.chat.id.0)) + .set(remind_days_ahead.eq(days_ahead)) + .execute(db)?; + Ok(()) + })?; + + Ok(()) +} + pub async fn fetch_and_announce_appointment( bot: &Throttle, chat_info: &mut ChatInfo, diff --git a/src/db.rs b/src/db.rs index ad249e2..e4cd4ba 100644 --- a/src/db.rs +++ b/src/db.rs @@ -13,6 +13,7 @@ pub struct DbChat { last_reminder: Option, pinned_message: Option, locale: Option, + remind_days_ahead: i64, } pub struct ChatInfo { @@ -22,6 +23,7 @@ pub struct ChatInfo { pub last_reminder: Option>, pub pinned_message_id: Option, pub locale: String, + pub remind_days_ahead: u64, } impl From for ChatInfo { @@ -41,6 +43,8 @@ impl From for ChatInfo { let locale = db_chat.locale.unwrap_or("de".into()); + let remind_days_ahead = db_chat.remind_days_ahead.try_into().unwrap_or(0); + ChatInfo { id: db_chat.telegram_id, calendar: db_chat.calendar, @@ -48,6 +52,7 @@ impl From for ChatInfo { last_reminder, pinned_message_id: db_chat.pinned_message, locale, + remind_days_ahead, } } } diff --git a/src/main.rs b/src/main.rs index 838de55..a71a4a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use std::{env, fs::File, io::BufReader, sync::Arc}; use anyhow::Result; use async_mutex::Mutex; use bot::fetch_and_announce_appointment; -use chrono::{DateTime, NaiveTime, TimeZone, Utc}; +use chrono::{DateTime, Days, NaiveTime, TimeZone, Utc}; use chrono_tz::Europe; use db::ChatInfo; use diesel::result::Error::NotFound; @@ -41,6 +41,8 @@ pub struct Config { poll_interval: u64, #[serde(deserialize_with = "deserialize_time")] reminder_time: NaiveTime, + #[serde(deserialize_with = "deserialize_time")] + preceeding_day_reminder_time: NaiveTime, } fn deserialize_time<'de, D: Deserializer<'de>>(deserializer: D) -> Result { @@ -79,7 +81,7 @@ async fn main() { db.run_pending_migrations(MIGRATIONS).unwrap(); let db = Arc::new(Mutex::new(db)); - let bot = Bot::new(config.token).throttle(Limits::default()); + let bot = Bot::new(&config.token).throttle(Limits::default()); { let db = db.clone(); @@ -104,16 +106,18 @@ async fn main() { Err(e) => Err(e).unwrap(), }; + let next_appointment = + next_appointment.map(|timestamp| Utc.timestamp_opt(timestamp, 0).unwrap()); + let sleep_duration = next_appointment - .map(|timestamp| Utc.timestamp_opt(timestamp, 0).unwrap()) - .map(|date_time| date_time - now) + .map(|next_appointment| next_appointment - now) .map(|duration| duration.to_std().unwrap()) .filter(|duration| *duration < poll_duration) .unwrap_or(poll_duration); sleep(sleep_duration).await; - let result = check_task(&bot, config.reminder_time, &db).await; + let result = check_task(&bot, &config, &db).await; if let Err(e) = result { error!("{}\nBacktrace:\n{}", e, e.backtrace()); } @@ -131,14 +135,15 @@ struct Reminder { text: String, } -async fn check_task(bot: &Throttle, reminder_time: NaiveTime, db: &Database) -> Result<()> { +// Checks if the date of the next appointment has changed (and announces if so) +// Additionally, checks if it is time for a reminder and sends that reminder if necessary +async fn check_task(bot: &Throttle, config: &Config, db: &Database) -> Result<()> { let chats = db.lock().await.transaction(|db| { use schema::chat::dsl::*; chat.load::(db) })?; let now = Utc::now().with_timezone(&Europe::Berlin); - let today = now.date_naive(); for chat in chats { let mut chat_info = ChatInfo::from(chat); @@ -150,10 +155,6 @@ async fn check_task(bot: &Throttle, reminder_time: NaiveTime, db: &Database }; let appointment = appointment.with_timezone(&Europe::Berlin); - if appointment.start.date_naive() != today { - continue; - } - let mut reminder = None; if now >= appointment.start { reminder = Some(Reminder { @@ -161,24 +162,39 @@ async fn check_task(bot: &Throttle, reminder_time: NaiveTime, db: &Database text: t!("messages.starting_now", locale = &chat_info.locale), }); } else { - let reminder_date_time = now - .date_naive() - .and_time(reminder_time) + let reminder_day = + appointment.start.date_naive() - Days::new(chat_info.remind_days_ahead); + let reminder_date_time = if chat_info.remind_days_ahead == 0 { + reminder_day.and_time(config.reminder_time) + } else { + reminder_day.and_time(config.preceeding_day_reminder_time) + }; + let reminder_date_time = reminder_date_time .and_local_timezone(now.timezone()) .unwrap(); if now >= reminder_date_time { + // TODO This can have weird effects if it's happenig around midnight, since it's not timezone aware (and may even mix multiple timezones) + let remaining_time = appointment.start.date_naive() - now.date_naive(); + let remaining_days = remaining_time.num_days(); + let message_id = match remaining_days { + 0 => "messages.appointment_today", + 1 => "messages.appointment_tomorrow", + _ => "messages.appointment_soon", + }; + let reminder_text = t!( + message_id, + locale = &chat_info.locale, + no_days = remaining_days, + start_time = &appointment.start.format("%H:%M").to_string(), + uk_time = &appointment + .start + .with_timezone(&Europe::London) + .format("%H:%M") + .to_string() + ); reminder = Some(Reminder { time: reminder_date_time, - text: t!( - "messages.appointment_today", - locale = &chat_info.locale, - start_time = &appointment.start.format("%H:%M").to_string(), - uk_time = &appointment - .start - .with_timezone(&Europe::London) - .format("%H:%M") - .to_string() - ), + text: reminder_text, }) } } diff --git a/src/schema.rs b/src/schema.rs index ee13523..aa5af55 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,14 +1,15 @@ // @generated automatically by Diesel CLI. diesel::table! { - chat (id) { - id -> Integer, - telegram_id -> BigInt, - calendar -> Text, - next_appointment_start -> Nullable, - next_appointment_end -> Nullable, - last_reminder -> Nullable, - pinned_message_id -> Nullable, - locale -> Nullable, - } + chat (id) { + id -> Integer, + telegram_id -> BigInt, + calendar -> Text, + next_appointment_start -> Nullable, + next_appointment_end -> Nullable, + last_reminder -> Nullable, + pinned_message_id -> Nullable, + locale -> Nullable, + remind_days_ahead -> BigInt, + } }