Allow granular per-chat reminder configuration

This commit is contained in:
2024-02-11 10:30:20 +01:00
parent d338f89a15
commit 454bcf5307
10 changed files with 359 additions and 108 deletions

159
src/db.rs
View File

@@ -1,12 +1,20 @@
use chrono::{DateTime, TimeZone, Utc};
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 {
@@ -26,7 +34,135 @@ pub struct DbChat {
pub struct DbReminder {
id: i32,
chat_id: i32,
days_ahead: i64,
reminder_delta_hours: i64,
reminder_time: Option<String>,
}
#[derive(Insertable)]
#[diesel(table_name = reminders)]
pub struct NewDbReminder {
chat_id: i32,
reminder_delta_hours: i64,
reminder_time: Option<String>,
}
pub struct Reminder {
pub delta: Duration,
pub time: Option<NaiveTime>,
}
impl Reminder {
pub fn to_localized_str<S: AsRef<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_string = t!(
&format!("reminders.{}.{}", delta_mode, pluralization),
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,
);
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<Regex> =
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<Self, Self::Err> {
// 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<DbReminder> for Reminder {
type Error = chrono::ParseError;
fn try_from(db_reminder: DbReminder) -> Result<Self, Self::Error> {
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<Tz: TimeZone> {
@@ -37,11 +173,14 @@ pub struct ChatInfo<Tz: TimeZone> {
pub last_reminder: Option<DateTime<Tz>>,
pub pinned_message_id: Option<i32>,
pub locale: String,
pub remind_days_ahead: Vec<u64>,
pub reminders: Vec<Reminder>,
}
impl ChatInfo<Utc> {
pub fn from_db(db_chat: DbChat, db_reminders: Vec<DbReminder>) -> Self {
pub fn from_db(
db_chat: DbChat,
db_reminders: Vec<DbReminder>,
) -> Result<Self, chrono::ParseError> {
let next_appointment = db_chat
.next_appointment_start
// Join appointments into single option
@@ -57,12 +196,12 @@ impl ChatInfo<Utc> {
let locale = db_chat.locale.unwrap_or("de".into());
let remind_days_ahead = db_reminders
let reminders = db_reminders
.into_iter()
.map(|reminder| reminder.days_ahead.try_into().unwrap_or(0))
.collect();
.map(Reminder::try_from)
.collect::<Result<_, _>>()?;
ChatInfo {
Ok(ChatInfo {
db_id: db_chat.id,
id: db_chat.telegram_id,
calendar: db_chat.calendar,
@@ -70,7 +209,7 @@ impl ChatInfo<Utc> {
last_reminder,
pinned_message_id: db_chat.pinned_message_id,
locale,
remind_days_ahead,
}
reminders,
})
}
}