214 lines
5.3 KiB
Rust
214 lines
5.3 KiB
Rust
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<i64>,
|
|
next_appointment_end: Option<i64>,
|
|
last_reminder: Option<i64>,
|
|
pinned_message_id: Option<i32>,
|
|
locale: Option<String>,
|
|
}
|
|
|
|
#[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<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_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<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> {
|
|
pub db_id: i32,
|
|
pub id: i64,
|
|
pub calendar: String,
|
|
pub next_appointment: Option<Appointment<Tz>>,
|
|
pub last_reminder: Option<DateTime<Tz>>,
|
|
pub pinned_message_id: Option<i32>,
|
|
pub locale: String,
|
|
pub reminders: Vec<Reminder>,
|
|
}
|
|
|
|
impl ChatInfo<Utc> {
|
|
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
|
|
.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::<Result<_, _>>()?;
|
|
|
|
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,
|
|
})
|
|
}
|
|
}
|