use std::str::FromStr; use chrono::{DateTime, Duration, NaiveTime, TimeZone, Utc}; use diesel::{ associations::{Associations, Identifiable}, prelude::Insertable, Queryable, Selectable, }; use once_cell::sync::Lazy; use regex::Regex; use thiserror::Error; use crate::appointment::Appointment; use crate::schema::{chats, reminders}; const TIME_FORMAT: &str = "%H:%M"; #[derive(Queryable, Selectable, Identifiable)] #[diesel(table_name = chats)] pub struct DbChat { id: i32, telegram_id: i64, calendar: String, next_appointment_start: Option, next_appointment_end: Option, last_reminder: Option, pinned_message_id: Option, locale: Option, } #[derive(Queryable, Selectable, Identifiable, Associations)] #[diesel(belongs_to(DbChat, foreign_key=chat_id))] #[diesel(table_name = reminders)] pub struct DbReminder { id: i32, chat_id: i32, reminder_delta_hours: i64, reminder_time: Option, } #[derive(Insertable)] #[diesel(table_name = reminders)] pub struct NewDbReminder { chat_id: i32, reminder_delta_hours: i64, reminder_time: Option, } pub struct Reminder { pub delta: Duration, pub time: Option, } impl Reminder { pub fn to_localized_str>(&self, locale: S) -> String { let delta_mode; let delta; if self.delta.num_hours() % 24 == 0 { delta_mode = "days"; delta = self.delta.num_days(); } else { delta_mode = "hours"; delta = self.delta.num_hours(); }; let pluralization = if delta == 1 { "one" } else { "other" }; let delta_key = format!("reminders.{}.{}", delta_mode, pluralization); let delta_string = t!(&delta_key, locale = locale.as_ref(), delta = delta,); let message_key = if self.delta.num_hours() > 0 { "reminders.delta" } else { "reminders.thisday_delta" }; let mut result = t!( message_key, locale = locale.as_ref(), delta_text = &delta_string, ) .into_owned(); if let Some(time) = self.time { result += &t!( "reminders.time", locale = locale.as_ref(), time = time.format(TIME_FORMAT), ); } result } pub fn to_db(&self, chat_id: i32) -> NewDbReminder { let reminder_time = self.time.map(|time| time.format(TIME_FORMAT).to_string()); let reminder_delta_hours = self.delta.num_hours(); NewDbReminder { chat_id, reminder_delta_hours, reminder_time, } } } static RE_REMINDER: Lazy = Lazy::new(|| Regex::new(r"^(\d+)([hHdD])(?:@(\d{1,2})(?:[:](\d{2}))?)?$").unwrap()); #[derive(Error, Debug)] pub enum ParseReminderError { #[error("The reminder format is invalid")] InvalidReminderFormat, #[error("The time is invalid")] InvalidTime, #[error("The delta is not allowed to be zero")] DeltaZero, } impl FromStr for Reminder { type Err = ParseReminderError; fn from_str(s: &str) -> Result { // TODO Better error messages let captures = RE_REMINDER .captures(s) .ok_or(ParseReminderError::InvalidReminderFormat)?; let mut delta = captures.get(1).unwrap().as_str().parse().unwrap(); if captures.get(2).unwrap().as_str().to_lowercase() == "d" { delta *= 24; } else { // Setting a fixed time is not allowed in hour-mode if captures.get(3).is_some() { return Err(ParseReminderError::InvalidReminderFormat); } } let time = captures .get(3) .map(|capture| { let hours = capture.as_str().parse().unwrap(); let minutes = captures .get(4) .map(|minutes| minutes.as_str().parse().unwrap()) .unwrap_or(0); NaiveTime::from_hms_opt(hours, minutes, 0).ok_or(ParseReminderError::InvalidTime) }) .transpose()?; if delta == 0 && time.is_none() { return Err(ParseReminderError::DeltaZero); } let delta = Duration::hours(delta); Ok(Self { delta, time }) } } impl TryFrom for Reminder { type Error = chrono::ParseError; fn try_from(db_reminder: DbReminder) -> Result { let time = db_reminder .reminder_time .as_ref() .map(|s| NaiveTime::parse_from_str(s, TIME_FORMAT)) .transpose()?; let delta = Duration::hours(db_reminder.reminder_delta_hours); Ok(Self { delta, time }) } } pub struct ChatInfo { pub db_id: i32, pub id: i64, pub calendar: String, pub next_appointment: Option>, pub last_reminder: Option>, pub pinned_message_id: Option, pub locale: String, pub reminders: Vec, } impl ChatInfo { pub fn from_db( db_chat: DbChat, db_reminders: Vec, ) -> Result { let next_appointment = db_chat .next_appointment_start // Join appointments into single option .and_then(|start| Some([start, db_chat.next_appointment_end?])) // Convert timestamps to datetimes .map(|timestamps| timestamps.map(|timestamp| Utc.timestamp_opt(timestamp, 0).unwrap())) // Join datetimes into Appointment .map(|[start, end]| Appointment { start, end }); let last_reminder = db_chat .last_reminder .map(|timestamp| Utc.timestamp_opt(timestamp, 0).unwrap()); let locale = db_chat.locale.unwrap_or("de".into()); let reminders = db_reminders .into_iter() .map(Reminder::try_from) .collect::>()?; Ok(ChatInfo { db_id: db_chat.id, id: db_chat.telegram_id, calendar: db_chat.calendar, next_appointment: next_appointment, last_reminder, pinned_message_id: db_chat.pinned_message_id, locale, reminders, }) } }