Files
telegram-bot-calendar/src/db.rs

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,
})
}
}