use crate::Error; use crate::calendar::CalendarResourceService; use axum::body::Body; use axum::extract::State; use axum::{extract::Path, response::Response}; use headers::{ContentType, HeaderMapExt}; use http::{HeaderValue, Method, StatusCode, header}; use ical::builder::calendar::IcalCalendarBuilder; use ical::component::CalendarInnerData; use ical::generator::Emitter; use ical::property::Property; use percent_encoding::{CONTROLS, utf8_percent_encode}; use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal}; use std::collections::HashMap; use std::str::FromStr; use tracing::instrument; #[instrument(skip(cal_store))] pub async fn route_get( Path((principal, calendar_id)): Path<(String, String)>, State(CalendarResourceService { cal_store, .. }): State>, user: Principal, method: Method, ) -> Result { if !user.is_principal(&principal) { return Err(crate::Error::Unauthorized); } let calendar = cal_store .get_calendar(&principal, &calendar_id, true) .await?; if !user.is_principal(&calendar.principal) { return Err(crate::Error::Unauthorized); } let mut vtimezones = HashMap::new(); let objects = cal_store.get_objects(&principal, &calendar_id).await?; let mut ical_calendar_builder = IcalCalendarBuilder::version("2.0") .gregorian() .prodid("RustiCal"); if let Some(displayname) = calendar.meta.displayname { ical_calendar_builder = ical_calendar_builder.set(Property { name: "X-WR-CALNAME".to_owned(), value: Some(displayname), params: vec![], }); } if let Some(description) = calendar.meta.description { ical_calendar_builder = ical_calendar_builder.set(Property { name: "X-WR-CALDESC".to_owned(), value: Some(description), params: vec![], }); } if let Some(timezone_id) = calendar.timezone_id { ical_calendar_builder = ical_calendar_builder.set(Property { name: "X-WR-TIMEZONE".to_owned(), value: Some(timezone_id), params: vec![], }); } for (_object_id, object) in &objects { vtimezones.extend(object.get_inner().get_vtimezones()); match object.get_inner().get_inner() { CalendarInnerData::Event(main, overrides) => { ical_calendar_builder = ical_calendar_builder .add_event(main.clone()) .add_events(overrides.iter().cloned()); } CalendarInnerData::Todo(main, overrides) => { ical_calendar_builder = ical_calendar_builder .add_todo(main.clone()) .add_todos(overrides.iter().cloned()); } CalendarInnerData::Journal(main, overrides) => { ical_calendar_builder = ical_calendar_builder .add_journal(main.clone()) .add_journals(overrides.iter().cloned()); } } } ical_calendar_builder = ical_calendar_builder.add_timezones(vtimezones.into_values().cloned()); let ical_calendar = ical_calendar_builder .build() .map_err(|parser_error| Error::IcalError(parser_error.into()))?; let mut resp = Response::builder().status(StatusCode::OK); let hdrs = resp.headers_mut().unwrap(); hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap()); let filename = format!("{}_{}.ics", calendar.principal, calendar.id); let filename = utf8_percent_encode(&filename, CONTROLS); hdrs.insert( header::CONTENT_DISPOSITION, HeaderValue::from_str(&format!( "attachement; filename*=UTF-8''{filename}; filename={filename}", )) .unwrap(), ); if matches!(method, Method::HEAD) { Ok(resp.body(Body::empty()).unwrap()) } else { Ok(resp.body(Body::new(ical_calendar.generate())).unwrap()) } }