botserver/src/calendar/mod.rs

1012 lines
33 KiB
Rust

use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::{Html, IntoResponse, Json},
routing::{get, post},
Router,
};
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use diesel::prelude::*;
use icalendar::{
Calendar, CalendarDateTime, Component, DatePerhapsTime, Event as IcalEvent, EventLike, Property,
};
use log::info;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use crate::core::shared::schema::{calendar_event_attendees, calendar_events, calendar_shares, calendars};
use crate::core::urls::ApiUrls;
use crate::shared::state::AppState;
pub mod caldav;
pub mod ui;
fn get_bot_context() -> (Uuid, Uuid) {
let org_id = std::env::var("DEFAULT_ORG_ID")
.ok()
.and_then(|s| Uuid::parse_str(&s).ok())
.unwrap_or_else(Uuid::nil);
let bot_id = std::env::var("DEFAULT_BOT_ID")
.ok()
.and_then(|s| Uuid::parse_str(&s).ok())
.unwrap_or_else(Uuid::nil);
(org_id, bot_id)
}
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable, Insertable, AsChangeset)]
#[diesel(table_name = calendars)]
pub struct CalendarRecord {
pub id: Uuid,
pub org_id: Uuid,
pub bot_id: Uuid,
pub owner_id: Uuid,
pub name: String,
pub description: Option<String>,
pub color: Option<String>,
pub timezone: Option<String>,
pub is_primary: bool,
pub is_visible: bool,
pub is_shared: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable, Insertable, AsChangeset)]
#[diesel(table_name = calendar_events)]
pub struct CalendarEventRecord {
pub id: Uuid,
pub org_id: Uuid,
pub bot_id: Uuid,
pub calendar_id: Uuid,
pub owner_id: Uuid,
pub title: String,
pub description: Option<String>,
pub location: Option<String>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub all_day: bool,
pub recurrence_rule: Option<String>,
pub recurrence_id: Option<Uuid>,
pub color: Option<String>,
pub status: String,
pub visibility: String,
pub busy_status: String,
pub reminders: serde_json::Value,
pub attendees: serde_json::Value,
pub conference_data: Option<serde_json::Value>,
pub metadata: serde_json::Value,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable, Insertable)]
#[diesel(table_name = calendar_event_attendees)]
pub struct EventAttendeeRecord {
pub id: Uuid,
pub event_id: Uuid,
pub email: String,
pub name: Option<String>,
pub status: String,
pub role: String,
pub rsvp_time: Option<DateTime<Utc>>,
pub comment: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable, Insertable)]
#[diesel(table_name = calendar_shares)]
pub struct CalendarShareRecord {
pub id: Uuid,
pub calendar_id: Uuid,
pub shared_with_user_id: Option<Uuid>,
pub shared_with_email: Option<String>,
pub permission: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarEvent {
pub id: Uuid,
pub calendar_id: Uuid,
pub title: String,
pub description: Option<String>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub location: Option<String>,
pub attendees: Vec<String>,
pub organizer: String,
pub reminder_minutes: Option<i32>,
pub recurrence: Option<String>,
pub all_day: bool,
pub status: String,
pub color: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarEventInput {
pub calendar_id: Option<Uuid>,
pub title: String,
pub description: Option<String>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub location: Option<String>,
#[serde(default)]
pub attendees: Vec<String>,
pub organizer: String,
pub reminder_minutes: Option<i32>,
pub recurrence: Option<String>,
#[serde(default)]
pub all_day: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateCalendarRequest {
pub name: String,
pub description: Option<String>,
pub color: Option<String>,
pub timezone: Option<String>,
#[serde(default)]
pub is_primary: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateCalendarRequest {
pub name: Option<String>,
pub description: Option<String>,
pub color: Option<String>,
pub timezone: Option<String>,
pub is_visible: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EventQuery {
pub calendar_id: Option<Uuid>,
pub start: Option<DateTime<Utc>>,
pub end: Option<DateTime<Utc>>,
pub limit: Option<i64>,
pub offset: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareCalendarRequest {
pub user_id: Option<Uuid>,
pub email: Option<String>,
pub permission: String,
}
impl CalendarEvent {
pub fn to_ical(&self) -> IcalEvent {
let mut event = IcalEvent::new();
event.uid(&self.id.to_string());
event.summary(&self.title);
event.starts(self.start_time);
event.ends(self.end_time);
if let Some(ref desc) = self.description {
event.description(desc);
}
if let Some(ref loc) = self.location {
event.location(loc);
}
event.add_property("ORGANIZER", format!("mailto:{}", self.organizer));
for attendee in &self.attendees {
event.add_property("ATTENDEE", format!("mailto:{attendee}"));
}
if let Some(ref rrule) = self.recurrence {
event.add_property("RRULE", rrule);
}
if let Some(minutes) = self.reminder_minutes {
event.add_property("VALARM", format!("-PT{minutes}M"));
}
event.done()
}
pub fn from_ical(ical: &IcalEvent, organizer: &str, calendar_id: Uuid) -> Option<Self> {
let uid = ical.get_uid()?;
let summary = ical.get_summary()?;
let start_time = date_perhaps_time_to_utc(ical.get_start()?)?;
let end_time = date_perhaps_time_to_utc(ical.get_end()?)?;
let id = Uuid::parse_str(uid).unwrap_or_else(|_| Uuid::new_v4());
Some(Self {
id,
calendar_id,
title: summary.to_string(),
description: ical.get_description().map(String::from),
start_time,
end_time,
location: ical.get_location().map(String::from),
attendees: Vec::new(),
organizer: organizer.to_string(),
reminder_minutes: None,
recurrence: None,
all_day: false,
status: "confirmed".to_string(),
color: None,
created_at: Utc::now(),
updated_at: Utc::now(),
})
}
}
fn date_perhaps_time_to_utc(dpt: DatePerhapsTime) -> Option<DateTime<Utc>> {
match dpt {
DatePerhapsTime::DateTime(cal_dt) => match cal_dt {
CalendarDateTime::Utc(dt) => Some(dt),
CalendarDateTime::Floating(naive) => Some(Utc.from_utc_datetime(&naive)),
CalendarDateTime::WithTimezone { date_time, .. } => {
Some(Utc.from_utc_datetime(&date_time))
}
},
DatePerhapsTime::Date(date) => {
let naive = NaiveDateTime::new(date, chrono::NaiveTime::from_hms_opt(0, 0, 0)?);
Some(Utc.from_utc_datetime(&naive))
}
}
}
fn record_to_event(record: CalendarEventRecord) -> CalendarEvent {
let attendees: Vec<String> = serde_json::from_value(record.attendees.clone()).unwrap_or_default();
let reminders: Vec<serde_json::Value> = serde_json::from_value(record.reminders.clone()).unwrap_or_default();
let reminder_minutes = reminders.first()
.and_then(|r| r.get("minutes_before"))
.and_then(|m| m.as_i64())
.map(|m| m as i32);
CalendarEvent {
id: record.id,
calendar_id: record.calendar_id,
title: record.title,
description: record.description,
start_time: record.start_time,
end_time: record.end_time,
location: record.location,
attendees,
organizer: record.owner_id.to_string(),
reminder_minutes,
recurrence: record.recurrence_rule,
all_day: record.all_day,
status: record.status,
color: record.color,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
pub fn export_to_ical(events: &[CalendarEvent], calendar_name: &str) -> String {
let mut calendar = Calendar::new();
calendar.name(calendar_name);
calendar.append_property(Property::new("PRODID", "-//GeneralBots//Calendar//EN"));
for event in events {
calendar.push(event.to_ical());
}
calendar.done().to_string()
}
pub fn import_from_ical(ical_str: &str, organizer: &str, calendar_id: Uuid) -> Vec<CalendarEvent> {
let Ok(calendar) = ical_str.parse::<Calendar>() else {
return Vec::new();
};
calendar
.components
.iter()
.filter_map(|c| {
if let icalendar::CalendarComponent::Event(e) = c {
CalendarEvent::from_ical(e, organizer, calendar_id)
} else {
None
}
})
.collect()
}
pub async fn create_calendar(
State(state): State<Arc<AppState>>,
Json(input): Json<CreateCalendarRequest>,
) -> Result<Json<CalendarRecord>, StatusCode> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let owner_id = Uuid::nil();
let now = Utc::now();
let new_calendar = CalendarRecord {
id: Uuid::new_v4(),
org_id,
bot_id,
owner_id,
name: input.name,
description: input.description,
color: input.color.or(Some("#3b82f6".to_string())),
timezone: input.timezone.or(Some("UTC".to_string())),
is_primary: input.is_primary,
is_visible: true,
is_shared: false,
created_at: now,
updated_at: now,
};
let calendar = new_calendar.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
diesel::insert_into(calendars::table)
.values(&new_calendar)
.execute(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok::<_, StatusCode>(())
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
result?;
info!("Created calendar: {} ({})", calendar.name, calendar.id);
Ok(Json(calendar))
}
pub async fn list_calendars_db(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<CalendarRecord>>, StatusCode> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
calendars::table
.filter(calendars::org_id.eq(org_id))
.filter(calendars::bot_id.eq(bot_id))
.order(calendars::created_at.desc())
.load::<CalendarRecord>(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(result?))
}
pub async fn get_calendar(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> Result<Json<CalendarRecord>, StatusCode> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
calendars::table
.find(id)
.first::<CalendarRecord>(&mut conn)
.optional()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
result?.ok_or(StatusCode::NOT_FOUND).map(Json)
}
pub async fn update_calendar(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
Json(input): Json<UpdateCalendarRequest>,
) -> Result<Json<CalendarRecord>, StatusCode> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
let mut calendar = calendars::table
.find(id)
.first::<CalendarRecord>(&mut conn)
.optional()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if let Some(name) = input.name {
calendar.name = name;
}
if let Some(description) = input.description {
calendar.description = Some(description);
}
if let Some(color) = input.color {
calendar.color = Some(color);
}
if let Some(timezone) = input.timezone {
calendar.timezone = Some(timezone);
}
if let Some(is_visible) = input.is_visible {
calendar.is_visible = is_visible;
}
calendar.updated_at = Utc::now();
diesel::update(calendars::table.find(id))
.set(&calendar)
.execute(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok::<_, StatusCode>(calendar)
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(result?))
}
pub async fn delete_calendar(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> StatusCode {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
let deleted = diesel::delete(calendars::table.find(id))
.execute(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if deleted > 0 {
Ok::<_, StatusCode>(StatusCode::NO_CONTENT)
} else {
Ok(StatusCode::NOT_FOUND)
}
})
.await;
match result {
Ok(Ok(status)) => status,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub async fn list_events(
State(state): State<Arc<AppState>>,
Query(query): Query<EventQuery>,
) -> Result<Json<Vec<CalendarEvent>>, StatusCode> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
let mut db_query = calendar_events::table
.filter(calendar_events::org_id.eq(org_id))
.filter(calendar_events::bot_id.eq(bot_id))
.into_boxed();
if let Some(calendar_id) = query.calendar_id {
db_query = db_query.filter(calendar_events::calendar_id.eq(calendar_id));
}
if let Some(start) = query.start {
db_query = db_query.filter(calendar_events::start_time.ge(start));
}
if let Some(end) = query.end {
db_query = db_query.filter(calendar_events::end_time.le(end));
}
db_query = db_query.order(calendar_events::start_time.asc());
if let Some(limit) = query.limit {
db_query = db_query.limit(limit);
}
if let Some(offset) = query.offset {
db_query = db_query.offset(offset);
}
db_query
.load::<CalendarEventRecord>(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let records = result?;
let events: Vec<CalendarEvent> = records.into_iter().map(record_to_event).collect();
Ok(Json(events))
}
pub async fn get_event(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> Result<Json<CalendarEvent>, StatusCode> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
calendar_events::table
.find(id)
.first::<CalendarEventRecord>(&mut conn)
.optional()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
result?.map(record_to_event).ok_or(StatusCode::NOT_FOUND).map(Json)
}
pub async fn create_event(
State(state): State<Arc<AppState>>,
Json(input): Json<CalendarEventInput>,
) -> Result<Json<CalendarEvent>, StatusCode> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let owner_id = Uuid::nil();
let now = Utc::now();
let calendar_id = input.calendar_id.unwrap_or_else(Uuid::nil);
let reminders = if let Some(minutes) = input.reminder_minutes {
serde_json::json!([{"minutes_before": minutes, "type": "notification"}])
} else {
serde_json::json!([])
};
let new_event = CalendarEventRecord {
id: Uuid::new_v4(),
org_id,
bot_id,
calendar_id,
owner_id,
title: input.title.clone(),
description: input.description.clone(),
location: input.location.clone(),
start_time: input.start_time,
end_time: input.end_time,
all_day: input.all_day,
recurrence_rule: input.recurrence.clone(),
recurrence_id: None,
color: None,
status: "confirmed".to_string(),
visibility: "default".to_string(),
busy_status: "busy".to_string(),
reminders,
attendees: serde_json::to_value(&input.attendees).unwrap_or(serde_json::json!([])),
conference_data: None,
metadata: serde_json::json!({}),
created_at: now,
updated_at: now,
};
let event_record = new_event.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
diesel::insert_into(calendar_events::table)
.values(&new_event)
.execute(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok::<_, StatusCode>(())
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
result?;
let event = record_to_event(event_record);
info!("Created calendar event: {} ({})", event.title, event.id);
Ok(Json(event))
}
pub async fn update_event(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
Json(input): Json<CalendarEventInput>,
) -> Result<Json<CalendarEvent>, StatusCode> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
let mut event = calendar_events::table
.find(id)
.first::<CalendarEventRecord>(&mut conn)
.optional()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
event.title = input.title;
event.description = input.description;
event.location = input.location;
event.start_time = input.start_time;
event.end_time = input.end_time;
event.all_day = input.all_day;
event.recurrence_rule = input.recurrence;
event.attendees = serde_json::to_value(&input.attendees).unwrap_or(serde_json::json!([]));
if let Some(minutes) = input.reminder_minutes {
event.reminders = serde_json::json!([{"minutes_before": minutes, "type": "notification"}]);
}
event.updated_at = Utc::now();
if let Some(calendar_id) = input.calendar_id {
event.calendar_id = calendar_id;
}
diesel::update(calendar_events::table.find(id))
.set(&event)
.execute(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok::<_, StatusCode>(event)
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let event = record_to_event(result?);
info!("Updated calendar event: {} ({})", event.title, event.id);
Ok(Json(event))
}
pub async fn delete_event(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> StatusCode {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
let deleted = diesel::delete(calendar_events::table.find(id))
.execute(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if deleted > 0 {
info!("Deleted calendar event: {id}");
Ok::<_, StatusCode>(StatusCode::NO_CONTENT)
} else {
Ok(StatusCode::NOT_FOUND)
}
})
.await;
match result {
Ok(Ok(status)) => status,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub async fn share_calendar(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
Json(input): Json<ShareCalendarRequest>,
) -> Result<Json<CalendarShareRecord>, StatusCode> {
let pool = state.conn.clone();
let new_share = CalendarShareRecord {
id: Uuid::new_v4(),
calendar_id: id,
shared_with_user_id: input.user_id,
shared_with_email: input.email,
permission: input.permission,
created_at: Utc::now(),
};
let share = new_share.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
diesel::insert_into(calendar_shares::table)
.values(&new_share)
.execute(&mut conn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok::<_, StatusCode>(())
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
result?;
Ok(Json(share))
}
pub async fn export_ical(
State(state): State<Arc<AppState>>,
Path(calendar_id): Path<Uuid>,
) -> impl IntoResponse {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().ok()?;
let calendar = calendars::table
.find(calendar_id)
.first::<CalendarRecord>(&mut conn)
.optional()
.ok()??;
let events = calendar_events::table
.filter(calendar_events::calendar_id.eq(calendar_id))
.load::<CalendarEventRecord>(&mut conn)
.ok()?;
let event_list: Vec<CalendarEvent> = events.into_iter().map(record_to_event).collect();
Some(export_to_ical(&event_list, &calendar.name))
})
.await;
match result {
Ok(Some(ical)) => (
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "text/calendar; charset=utf-8")],
ical,
).into_response(),
_ => StatusCode::NOT_FOUND.into_response(),
}
}
pub async fn import_ical(
State(state): State<Arc<AppState>>,
Path(calendar_id): Path<Uuid>,
body: String,
) -> Result<Json<serde_json::Value>, StatusCode> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let owner_id = Uuid::nil();
let events = import_from_ical(&body, &owner_id.to_string(), calendar_id);
let count = events.len();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
let now = Utc::now();
for event in events {
let record = CalendarEventRecord {
id: event.id,
org_id,
bot_id,
calendar_id,
owner_id,
title: event.title,
description: event.description,
location: event.location,
start_time: event.start_time,
end_time: event.end_time,
all_day: event.all_day,
recurrence_rule: event.recurrence,
recurrence_id: None,
color: event.color,
status: event.status,
visibility: "default".to_string(),
busy_status: "busy".to_string(),
reminders: serde_json::json!([]),
attendees: serde_json::to_value(&event.attendees).unwrap_or(serde_json::json!([])),
conference_data: None,
metadata: serde_json::json!({}),
created_at: now,
updated_at: now,
};
diesel::insert_into(calendar_events::table)
.values(&record)
.execute(&mut conn)
.ok();
}
Ok::<_, StatusCode>(())
})
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
result?;
Ok(Json(serde_json::json!({ "imported": count })))
}
pub async fn list_calendars_api(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().ok()?;
calendars::table
.filter(calendars::org_id.eq(org_id))
.filter(calendars::bot_id.eq(bot_id))
.load::<CalendarRecord>(&mut conn)
.ok()
})
.await;
match result {
Ok(Some(cals)) => {
let calendar_list: Vec<serde_json::Value> = cals.iter().map(|c| {
serde_json::json!({
"id": c.id,
"name": c.name,
"color": c.color,
"visible": c.is_visible
})
}).collect();
Json(serde_json::json!({ "calendars": calendar_list }))
}
_ => Json(serde_json::json!({
"calendars": [{
"id": "default",
"name": "My Calendar",
"color": "#3b82f6",
"visible": true
}]
})),
}
}
pub async fn list_calendars_html(State(state): State<Arc<AppState>>) -> Html<String> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().ok()?;
calendars::table
.filter(calendars::org_id.eq(org_id))
.filter(calendars::bot_id.eq(bot_id))
.load::<CalendarRecord>(&mut conn)
.ok()
})
.await;
match result {
Ok(Some(cals)) if !cals.is_empty() => {
let html: String = cals.iter().map(|c| {
let color = c.color.as_deref().unwrap_or("#3b82f6");
let checked = if c.is_visible { "checked" } else { "" };
format!(
r#"<div class="calendar-item" data-calendar-id="{}">
<span class="calendar-checkbox {}" style="background: {};" onclick="toggleCalendar(this)"></span>
<span class="calendar-name">{}</span>
</div>"#,
c.id, checked, color, c.name
)
}).collect();
Html(html)
}
_ => Html(r#"
<div class="calendar-item" data-calendar-id="default">
<span class="calendar-checkbox checked" style="background: #3b82f6;" onclick="toggleCalendar(this)"></span>
<span class="calendar-name">My Calendar</span>
</div>
"#.to_string()),
}
}
pub async fn upcoming_events_api(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let now = Utc::now();
let end = now + chrono::Duration::days(7);
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().ok()?;
calendar_events::table
.filter(calendar_events::org_id.eq(org_id))
.filter(calendar_events::bot_id.eq(bot_id))
.filter(calendar_events::start_time.ge(now))
.filter(calendar_events::start_time.le(end))
.order(calendar_events::start_time.asc())
.limit(10)
.load::<CalendarEventRecord>(&mut conn)
.ok()
})
.await;
match result {
Ok(Some(events)) => {
let event_list: Vec<serde_json::Value> = events.iter().map(|e| {
serde_json::json!({
"id": e.id,
"title": e.title,
"start_time": e.start_time,
"end_time": e.end_time,
"location": e.location
})
}).collect();
Json(serde_json::json!({ "events": event_list }))
}
_ => Json(serde_json::json!({
"events": [],
"message": "No upcoming events"
})),
}
}
pub async fn upcoming_events_html(State(state): State<Arc<AppState>>) -> Html<String> {
let pool = state.conn.clone();
let (org_id, bot_id) = get_bot_context();
let now = Utc::now();
let end = now + chrono::Duration::days(7);
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().ok()?;
calendar_events::table
.filter(calendar_events::org_id.eq(org_id))
.filter(calendar_events::bot_id.eq(bot_id))
.filter(calendar_events::start_time.ge(now))
.filter(calendar_events::start_time.le(end))
.order(calendar_events::start_time.asc())
.limit(5)
.load::<CalendarEventRecord>(&mut conn)
.ok()
})
.await;
match result {
Ok(Some(events)) if !events.is_empty() => {
let html: String = events.iter().map(|e| {
let color = e.color.as_deref().unwrap_or("#3b82f6");
let time = e.start_time.format("%b %d, %H:%M").to_string();
format!(
r#"<div class="upcoming-event">
<div class="upcoming-color" style="background: {};"></div>
<div class="upcoming-info">
<span class="upcoming-title">{}</span>
<span class="upcoming-time">{}</span>
</div>
</div>"#,
color, e.title, time
)
}).collect();
Html(html)
}
_ => Html(r#"
<div class="upcoming-event">
<div class="upcoming-color" style="background: #3b82f6;"></div>
<div class="upcoming-info">
<span class="upcoming-title">No upcoming events</span>
<span class="upcoming-time">Create your first event</span>
</div>
</div>
"#.to_string()),
}
}
pub async fn new_event_form() -> Html<String> {
Html(r#"
<div class="event-form-content">
<p>Create a new event using the form on the right panel.</p>
</div>
"#.to_string())
}
pub async fn new_calendar_form() -> Html<String> {
Html(r##"
<form class="calendar-form" hx-post="/api/calendar/calendars" hx-swap="none">
<div class="form-group">
<label>Calendar Name</label>
<input type="text" name="name" placeholder="My Calendar" required />
</div>
<div class="form-group">
<label>Color</label>
<div class="color-options">
<label><input type="radio" name="color" value="#3b82f6" checked /><span class="color-dot" style="background:#3b82f6"></span></label>
<label><input type="radio" name="color" value="#22c55e" /><span class="color-dot" style="background:#22c55e"></span></label>
<label><input type="radio" name="color" value="#f59e0b" /><span class="color-dot" style="background:#f59e0b"></span></label>
<label><input type="radio" name="color" value="#ef4444" /><span class="color-dot" style="background:#ef4444"></span></label>
<label><input type="radio" name="color" value="#8b5cf6" /><span class="color-dot" style="background:#8b5cf6"></span></label>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn-secondary" onclick="this.closest('.modal').classList.add('hidden')">Cancel</button>
<button type="submit" class="btn-primary">Create Calendar</button>
</div>
</form>
"##.to_string())
}
pub fn configure_calendar_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/api/calendar/calendars", get(list_calendars_db).post(create_calendar))
.route("/api/calendar/calendars/:id", get(get_calendar).put(update_calendar).delete(delete_calendar))
.route("/api/calendar/calendars/:id/share", post(share_calendar))
.route("/api/calendar/calendars/:id/export", get(export_ical))
.route("/api/calendar/calendars/:id/import", post(import_ical))
.route(ApiUrls::CALENDAR_EVENTS, get(list_events).post(create_event))
.route(ApiUrls::CALENDAR_EVENT_BY_ID, get(get_event).put(update_event).delete(delete_event))
.route(ApiUrls::CALENDAR_UPCOMING_JSON, get(upcoming_events_api))
}