mascarpone/src/web/ics.rs

89 lines
3.1 KiB
Rust
Raw Normal View History

2025-11-27 13:45:21 -06:00
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())
}
}