89 lines
3.1 KiB
Rust
89 lines
3.1 KiB
Rust
|
|
use axum::{Router, extract::Path, response::IntoResponse, routing::get};
|
||
|
|
use chrono::NaiveDate;
|
||
|
|
use icalendar::{Calendar, Component, Event, EventLike};
|
||
|
|
use regex::Regex;
|
||
|
|
|
||
|
|
use crate::models::user::{AuthSession, User};
|
||
|
|
use crate::models::{Birthday, HydratedContact};
|
||
|
|
use crate::{AppError, AppState, Database};
|
||
|
|
|
||
|
|
pub fn router() -> Router<AppState> {
|
||
|
|
Router::new().route("/cal/{path}", get(self::get::calendar))
|
||
|
|
}
|
||
|
|
|
||
|
|
mod get {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
pub async fn calendar(
|
||
|
|
auth_session: AuthSession,
|
||
|
|
Path(ics_path): Path<String>,
|
||
|
|
) -> Result<impl IntoResponse, AppError> {
|
||
|
|
let path_re = Regex::new(r"^(?<username>.+)-(?<hash>[0-9a-zA-Z]+).ics$").unwrap();
|
||
|
|
|
||
|
|
let username = if let Some(caps) = path_re.captures(&ics_path) {
|
||
|
|
caps.name("username").unwrap().as_str()
|
||
|
|
} else {
|
||
|
|
tracing::debug!(
|
||
|
|
"No username match in path {:?} for re /^.+-[0-9a-zA-Z]+.ics$/",
|
||
|
|
ics_path
|
||
|
|
);
|
||
|
|
return Err(AppError(anyhow::Error::msg("TODO: 404")));
|
||
|
|
};
|
||
|
|
|
||
|
|
let user: Option<User> = auth_session.backend.find_user(username).await?;
|
||
|
|
if user.is_none() {
|
||
|
|
tracing::debug!("No matching user for username {:?}", username);
|
||
|
|
return Err(AppError(anyhow::Error::msg("TODO: 404")));
|
||
|
|
}
|
||
|
|
|
||
|
|
let user = user.unwrap();
|
||
|
|
let pool = Database::for_user(&user).await?.pool;
|
||
|
|
let expected_path: (Option<String>,) = sqlx::query_as("select ics_path from settings")
|
||
|
|
.fetch_one(&pool)
|
||
|
|
.await?;
|
||
|
|
let debug_ics_path = ics_path.clone();
|
||
|
|
if expected_path.0 != Some(ics_path) {
|
||
|
|
tracing::debug!(
|
||
|
|
"Expected path {:?} did not match request path {:?}",
|
||
|
|
expected_path.0,
|
||
|
|
debug_ics_path
|
||
|
|
);
|
||
|
|
return Err(AppError(anyhow::Error::msg("TODO: 404")));
|
||
|
|
}
|
||
|
|
|
||
|
|
let calname = format!("Contact birthdays for {}", user.username);
|
||
|
|
let mut calendar = Calendar::new();
|
||
|
|
calendar.name(&calname);
|
||
|
|
calendar.append_property(("PRODID", "Mascarpone CRM"));
|
||
|
|
let contacts: Vec<HydratedContact> = sqlx::query_as(
|
||
|
|
"select id, birthday, (
|
||
|
|
select string_agg(name,'\x1c' order by sort)
|
||
|
|
from names where contact_id = c.id
|
||
|
|
) as names
|
||
|
|
from contacts c",
|
||
|
|
)
|
||
|
|
.fetch_all(&pool)
|
||
|
|
.await?;
|
||
|
|
for contact in &contacts {
|
||
|
|
if let Some(Birthday::Date(yo_date)) = &contact.birthday {
|
||
|
|
if let Some(date) = NaiveDate::from_ymd_opt(
|
||
|
|
yo_date.year.unwrap_or(1900),
|
||
|
|
yo_date.month,
|
||
|
|
yo_date.day,
|
||
|
|
) {
|
||
|
|
calendar.push(
|
||
|
|
Event::new()
|
||
|
|
.starts(date) // start-with-no-end is "all day"
|
||
|
|
.summary(&format!("{}'s Birthday", &contact.display_name()))
|
||
|
|
.add_property("RRULE", "FREQ=YEARLY"),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
tracing::debug!("{}", calendar);
|
||
|
|
|
||
|
|
Ok(calendar.to_string())
|
||
|
|
}
|
||
|
|
}
|