2025-11-22 22:55:35 -03:00
|
|
|
use axum::{
|
|
|
|
|
extract::{Path, Query, State},
|
|
|
|
|
http::StatusCode,
|
|
|
|
|
response::Json,
|
|
|
|
|
routing::{delete, get, post, put},
|
|
|
|
|
Router,
|
|
|
|
|
};
|
|
|
|
|
use chrono::{DateTime, Utc};
|
|
|
|
|
use diesel::prelude::*;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
use crate::shared::utils::DbPool;
|
|
|
|
|
use tokio::sync::RwLock;
|
|
|
|
|
use uuid::Uuid;
|
2025-11-26 22:54:22 -03:00
|
|
|
use crate::shared::state::AppState;
|
|
|
|
|
use diesel::sql_query;
|
|
|
|
|
use diesel::sql_types::{Text, Timestamptz, Integer, Jsonb};
|
2025-11-22 22:55:35 -03:00
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, QueryableByName)]
|
2025-11-22 22:55:35 -03:00
|
|
|
pub struct CalendarEvent {
|
|
|
|
|
pub 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_rule: Option<String>,
|
|
|
|
|
pub status: EventStatus,
|
|
|
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
|
pub updated_at: DateTime<Utc>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum EventStatus {
|
|
|
|
|
Scheduled,
|
|
|
|
|
InProgress,
|
|
|
|
|
Completed,
|
|
|
|
|
Cancelled,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Meeting {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub event_id: Uuid,
|
|
|
|
|
pub meeting_url: Option<String>,
|
|
|
|
|
pub meeting_id: Option<String>,
|
|
|
|
|
pub platform: MeetingPlatform,
|
|
|
|
|
pub recording_url: Option<String>,
|
|
|
|
|
pub notes: Option<String>,
|
|
|
|
|
pub action_items: Vec<ActionItem>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum MeetingPlatform {
|
|
|
|
|
Zoom,
|
|
|
|
|
Teams,
|
|
|
|
|
Meet,
|
|
|
|
|
Internal,
|
|
|
|
|
Other(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ActionItem {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub assignee: String,
|
|
|
|
|
pub due_date: Option<DateTime<Utc>>,
|
|
|
|
|
pub completed: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct CalendarReminder {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub event_id: Uuid,
|
|
|
|
|
pub remind_at: DateTime<Utc>,
|
|
|
|
|
pub message: String,
|
|
|
|
|
pub channel: ReminderChannel,
|
|
|
|
|
pub sent: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum ReminderChannel {
|
|
|
|
|
Email,
|
|
|
|
|
Sms,
|
|
|
|
|
Push,
|
|
|
|
|
InApp,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 09:38:50 -03:00
|
|
|
// API Request/Response structs
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct CreateEventRequest {
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub start_time: DateTime<Utc>,
|
|
|
|
|
pub end_time: DateTime<Utc>,
|
|
|
|
|
pub location: Option<String>,
|
|
|
|
|
pub attendees: Option<Vec<String>>,
|
|
|
|
|
pub organizer: String,
|
|
|
|
|
pub reminder_minutes: Option<i32>,
|
|
|
|
|
pub recurrence_rule: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct UpdateEventRequest {
|
|
|
|
|
pub title: Option<String>,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub start_time: Option<DateTime<Utc>>,
|
|
|
|
|
pub end_time: Option<DateTime<Utc>>,
|
|
|
|
|
pub location: Option<String>,
|
|
|
|
|
pub status: Option<EventStatus>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ScheduleMeetingRequest {
|
|
|
|
|
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 meeting_url: Option<String>,
|
|
|
|
|
pub meeting_id: Option<String>,
|
|
|
|
|
pub platform: Option<MeetingPlatform>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct SetReminderRequest {
|
|
|
|
|
pub event_id: Uuid,
|
|
|
|
|
pub remind_at: DateTime<Utc>,
|
|
|
|
|
pub message: String,
|
|
|
|
|
pub channel: ReminderChannel,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct EventListQuery {
|
|
|
|
|
pub start_date: Option<DateTime<Utc>>,
|
|
|
|
|
pub end_date: Option<DateTime<Utc>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct EventSearchQuery {
|
|
|
|
|
pub query: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct CheckAvailabilityQuery {
|
|
|
|
|
pub start_time: DateTime<Utc>,
|
|
|
|
|
pub end_time: DateTime<Utc>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct CalendarEngine {
|
|
|
|
|
db: Arc<DbPool>,
|
|
|
|
|
cache: Arc<RwLock<Vec<CalendarEvent>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CalendarEngine {
|
|
|
|
|
pub fn new(db: Arc<PgPool>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
db,
|
|
|
|
|
cache: Arc::new(RwLock::new(Vec::new())),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn create_event(
|
|
|
|
|
&self,
|
|
|
|
|
event: CalendarEvent,
|
|
|
|
|
) -> Result<CalendarEvent, Box<dyn std::error::Error>> {
|
2025-11-26 22:54:22 -03:00
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let attendees_json = serde_json::to_value(&event.attendees)?;
|
|
|
|
|
let recurrence_json = event.recurrence_rule.as_ref().map(|r| serde_json::to_value(r).ok()).flatten();
|
|
|
|
|
|
|
|
|
|
diesel::sql_query(
|
|
|
|
|
"INSERT INTO calendar_events
|
|
|
|
|
(id, title, description, start_time, end_time, location, attendees, organizer,
|
|
|
|
|
reminder_minutes, recurrence_rule, status, created_at, updated_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
|
|
|
RETURNING *"
|
|
|
|
|
)
|
2025-11-22 22:55:35 -03:00
|
|
|
event.id,
|
|
|
|
|
event.title,
|
|
|
|
|
event.description,
|
|
|
|
|
event.start_time,
|
|
|
|
|
event.end_time,
|
|
|
|
|
event.location,
|
|
|
|
|
&event.attendees[..],
|
|
|
|
|
event.organizer,
|
|
|
|
|
event.reminder_minutes,
|
|
|
|
|
event.recurrence_rule,
|
|
|
|
|
serde_json::to_value(&event.status)?,
|
|
|
|
|
event.created_at,
|
|
|
|
|
event.updated_at
|
|
|
|
|
)
|
|
|
|
|
.fetch_one(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
self.refresh_cache().await?;
|
|
|
|
|
|
|
|
|
|
Ok(event)
|
|
|
|
|
Ok(event)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn update_event(
|
|
|
|
|
&self,
|
|
|
|
|
id: Uuid,
|
|
|
|
|
updates: serde_json::Value,
|
|
|
|
|
) -> Result<CalendarEvent, Box<dyn std::error::Error>> {
|
|
|
|
|
let updated_at = Utc::now();
|
|
|
|
|
|
|
|
|
|
let result = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
UPDATE calendar_events
|
|
|
|
|
SET title = COALESCE($2, title),
|
|
|
|
|
description = COALESCE($3, description),
|
|
|
|
|
start_time = COALESCE($4, start_time),
|
|
|
|
|
end_time = COALESCE($5, end_time),
|
|
|
|
|
location = COALESCE($6, location),
|
|
|
|
|
updated_at = $7
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
RETURNING *
|
|
|
|
|
"#,
|
|
|
|
|
id,
|
|
|
|
|
updates.get("title").and_then(|v| v.as_str()),
|
|
|
|
|
updates.get("description").and_then(|v| v.as_str()),
|
|
|
|
|
updates
|
|
|
|
|
.get("start_time")
|
|
|
|
|
.and_then(|v| DateTime::parse_from_rfc3339(v.as_str()?).ok())
|
|
|
|
|
.map(|dt| dt.with_timezone(&Utc)),
|
|
|
|
|
updates
|
|
|
|
|
.get("end_time")
|
|
|
|
|
.and_then(|v| DateTime::parse_from_rfc3339(v.as_str()?).ok())
|
|
|
|
|
.map(|dt| dt.with_timezone(&Utc)),
|
|
|
|
|
updates.get("location").and_then(|v| v.as_str()),
|
|
|
|
|
updated_at
|
|
|
|
|
)
|
|
|
|
|
.fetch_one(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
self.refresh_cache().await?;
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::from_value(serde_json::to_value(result)?)?)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
pub async fn delete_event(&self, id: Uuid) -> Result<bool, Box<dyn std::error::Error>> {
|
|
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let rows_affected = diesel::sql_query("DELETE FROM calendar_events WHERE id = $1")
|
|
|
|
|
.bind::<diesel::sql_types::Uuid, _>(&id)
|
|
|
|
|
.execute(&mut conn)?;
|
2025-11-22 22:55:35 -03:00
|
|
|
|
|
|
|
|
self.refresh_cache().await?;
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
Ok(rows_affected > 0)
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_events_range(
|
|
|
|
|
&self,
|
|
|
|
|
start: DateTime<Utc>,
|
|
|
|
|
end: DateTime<Utc>,
|
|
|
|
|
) -> Result<Vec<CalendarEvent>, Box<dyn std::error::Error>> {
|
2025-11-26 22:54:22 -03:00
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let results = diesel::sql_query(
|
|
|
|
|
"SELECT * FROM calendar_events
|
|
|
|
|
WHERE start_time >= $1 AND end_time <= $2
|
|
|
|
|
ORDER BY start_time ASC"
|
|
|
|
|
)
|
|
|
|
|
.bind::<Timestamptz, _>(&start)
|
2025-11-22 22:55:35 -03:00
|
|
|
end
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_user_events(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: &str,
|
|
|
|
|
) -> Result<Vec<CalendarEvent>, Box<dyn std::error::Error>> {
|
2025-11-26 22:54:22 -03:00
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let results = diesel::sql_query(
|
|
|
|
|
"SELECT * FROM calendar_events
|
|
|
|
|
WHERE organizer = $1 OR $1::text = ANY(SELECT jsonb_array_elements_text(attendees))
|
|
|
|
|
ORDER BY start_time ASC"
|
2025-11-22 22:55:35 -03:00
|
|
|
)
|
2025-11-26 22:54:22 -03:00
|
|
|
.bind::<Text, _>(&user_id)
|
2025-11-22 22:55:35 -03:00
|
|
|
.fetch_all(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(results
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|r| serde_json::from_value(serde_json::to_value(r).unwrap()).unwrap())
|
|
|
|
|
.collect())
|
|
|
|
|
*/
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn create_meeting(
|
|
|
|
|
&self,
|
|
|
|
|
event_id: Uuid,
|
|
|
|
|
platform: MeetingPlatform,
|
|
|
|
|
) -> Result<Meeting, Box<dyn std::error::Error>> {
|
|
|
|
|
let meeting = Meeting {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
event_id,
|
|
|
|
|
meeting_url: None,
|
|
|
|
|
meeting_id: None,
|
|
|
|
|
platform,
|
|
|
|
|
recording_url: None,
|
|
|
|
|
notes: None,
|
|
|
|
|
action_items: Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
diesel::sql_query(
|
2025-11-22 22:55:35 -03:00
|
|
|
r#"
|
|
|
|
|
INSERT INTO meetings (id, event_id, platform, created_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
|
|
|
|
"#,
|
|
|
|
|
meeting.id,
|
|
|
|
|
meeting.event_id,
|
|
|
|
|
meeting.platform,
|
|
|
|
|
meeting.created_at
|
|
|
|
|
)
|
|
|
|
|
.execute(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
Ok(meeting)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn schedule_reminder(
|
|
|
|
|
&self,
|
|
|
|
|
event_id: Uuid,
|
|
|
|
|
minutes_before: i32,
|
|
|
|
|
channel: ReminderChannel,
|
|
|
|
|
) -> Result<CalendarReminder, Box<dyn std::error::Error>> {
|
|
|
|
|
let event = self.get_event(event_id).await?;
|
|
|
|
|
let remind_at = event.start_time - chrono::Duration::minutes(minutes_before as i64);
|
|
|
|
|
|
|
|
|
|
let reminder = CalendarReminder {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
event_id,
|
|
|
|
|
remind_at,
|
|
|
|
|
message: format!(
|
|
|
|
|
"Reminder: {} starts in {} minutes",
|
|
|
|
|
event.title, minutes_before
|
|
|
|
|
),
|
|
|
|
|
channel,
|
|
|
|
|
sent: false,
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
diesel::sql_query(
|
2025-11-22 22:55:35 -03:00
|
|
|
r#"
|
|
|
|
|
INSERT INTO calendar_reminders (id, event_id, remind_at, message, channel, sent)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
|
|
|
"#,
|
|
|
|
|
reminder.id,
|
|
|
|
|
reminder.event_id,
|
|
|
|
|
reminder.remind_at,
|
|
|
|
|
reminder.message,
|
|
|
|
|
reminder.channel,
|
|
|
|
|
reminder.sent
|
|
|
|
|
)
|
|
|
|
|
.execute(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
Ok(reminder)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
pub async fn get_event(&self, id: Uuid) -> Result<CalendarEvent, Box<dyn std::error::Error>> {
|
|
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
2025-11-22 22:55:35 -03:00
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
let result = diesel::sql_query("SELECT * FROM calendar_events WHERE id = $1")
|
|
|
|
|
.bind::<diesel::sql_types::Uuid, _>(&id)
|
|
|
|
|
.get_result::<CalendarEvent>(&mut conn)?;
|
|
|
|
|
|
|
|
|
|
Ok(result)
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn check_conflicts(
|
|
|
|
|
&self,
|
|
|
|
|
start: DateTime<Utc>,
|
|
|
|
|
end: DateTime<Utc>,
|
|
|
|
|
user_id: &str,
|
|
|
|
|
) -> Result<Vec<CalendarEvent>, Box<dyn std::error::Error>> {
|
2025-11-26 22:54:22 -03:00
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let results = diesel::sql_query(
|
|
|
|
|
"SELECT * FROM calendar_events
|
|
|
|
|
WHERE (organizer = $1 OR $1::text = ANY(SELECT jsonb_array_elements_text(attendees)))
|
|
|
|
|
AND NOT (end_time <= $2 OR start_time >= $3)"
|
|
|
|
|
)
|
|
|
|
|
.bind::<Text, _>(&user_id)
|
|
|
|
|
.bind::<Timestamptz, _>(&start)
|
2025-11-22 22:55:35 -03:00
|
|
|
end
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(results
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|r| serde_json::from_value(serde_json::to_value(r).unwrap()).unwrap())
|
|
|
|
|
.collect())
|
|
|
|
|
*/
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
2025-11-27 09:38:50 -03:00
|
|
|
pub async fn create_event(&self, event: CreateEventRequest) -> Result<CalendarEvent, Box<dyn std::error::Error>> {
|
|
|
|
|
let id = Uuid::new_v4();
|
|
|
|
|
let now = Utc::now();
|
|
|
|
|
|
|
|
|
|
let calendar_event = CalendarEvent {
|
|
|
|
|
id,
|
|
|
|
|
title: event.title,
|
|
|
|
|
description: event.description,
|
|
|
|
|
start_time: event.start_time,
|
|
|
|
|
end_time: event.end_time,
|
|
|
|
|
location: event.location,
|
|
|
|
|
attendees: event.attendees.unwrap_or_default(),
|
|
|
|
|
organizer: event.organizer,
|
|
|
|
|
reminder_minutes: event.reminder_minutes,
|
|
|
|
|
recurrence_rule: event.recurrence_rule,
|
|
|
|
|
status: EventStatus::Scheduled,
|
|
|
|
|
created_at: now,
|
|
|
|
|
updated_at: now,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Store in cache
|
|
|
|
|
self.cache.write().await.push(calendar_event.clone());
|
|
|
|
|
|
|
|
|
|
Ok(calendar_event)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn update_event(&self, id: Uuid, update: UpdateEventRequest) -> Result<CalendarEvent, Box<dyn std::error::Error>> {
|
|
|
|
|
let mut cache = self.cache.write().await;
|
|
|
|
|
|
|
|
|
|
if let Some(event) = cache.iter_mut().find(|e| e.id == id) {
|
|
|
|
|
if let Some(title) = update.title {
|
|
|
|
|
event.title = title;
|
|
|
|
|
}
|
|
|
|
|
if let Some(description) = update.description {
|
|
|
|
|
event.description = Some(description);
|
|
|
|
|
}
|
|
|
|
|
if let Some(start_time) = update.start_time {
|
|
|
|
|
event.start_time = start_time;
|
|
|
|
|
}
|
|
|
|
|
if let Some(end_time) = update.end_time {
|
|
|
|
|
event.end_time = end_time;
|
|
|
|
|
}
|
|
|
|
|
if let Some(location) = update.location {
|
|
|
|
|
event.location = Some(location);
|
|
|
|
|
}
|
|
|
|
|
if let Some(status) = update.status {
|
|
|
|
|
event.status = status;
|
|
|
|
|
}
|
|
|
|
|
event.updated_at = Utc::now();
|
|
|
|
|
|
|
|
|
|
Ok(event.clone())
|
|
|
|
|
} else {
|
|
|
|
|
Err("Event not found".into())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn delete_event(&self, id: Uuid) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
let mut cache = self.cache.write().await;
|
|
|
|
|
cache.retain(|e| e.id != id);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_events(&self, start_date: Option<DateTime<Utc>>, end_date: Option<DateTime<Utc>>) -> Result<Vec<CalendarEvent>, Box<dyn std::error::Error>> {
|
|
|
|
|
let cache = self.cache.read().await;
|
|
|
|
|
|
|
|
|
|
let events: Vec<CalendarEvent> = if let (Some(start), Some(end)) = (start_date, end_date) {
|
|
|
|
|
cache.iter()
|
|
|
|
|
.filter(|e| e.start_time >= start && e.start_time <= end)
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
} else {
|
|
|
|
|
cache.clone()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(events)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn search_events(&self, query: &str) -> Result<Vec<CalendarEvent>, Box<dyn std::error::Error>> {
|
|
|
|
|
let cache = self.cache.read().await;
|
|
|
|
|
let query_lower = query.to_lowercase();
|
|
|
|
|
|
|
|
|
|
let events: Vec<CalendarEvent> = cache
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|e| {
|
|
|
|
|
e.title.to_lowercase().contains(&query_lower) ||
|
|
|
|
|
e.description.as_ref().map_or(false, |d| d.to_lowercase().contains(&query_lower))
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(events)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn check_availability(&self, start_time: DateTime<Utc>, end_time: DateTime<Utc>) -> Result<bool, Box<dyn std::error::Error>> {
|
|
|
|
|
let cache = self.cache.read().await;
|
|
|
|
|
|
|
|
|
|
let has_conflict = cache.iter().any(|event| {
|
|
|
|
|
(event.start_time < end_time && event.end_time > start_time) &&
|
|
|
|
|
event.status != EventStatus::Cancelled
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(!has_conflict)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn schedule_meeting(&self, meeting: ScheduleMeetingRequest) -> Result<Meeting, Box<dyn std::error::Error>> {
|
|
|
|
|
// First create the calendar event
|
|
|
|
|
let event = self.create_event(CreateEventRequest {
|
|
|
|
|
title: meeting.title.clone(),
|
|
|
|
|
description: meeting.description.clone(),
|
|
|
|
|
start_time: meeting.start_time,
|
|
|
|
|
end_time: meeting.end_time,
|
|
|
|
|
location: meeting.location.clone(),
|
|
|
|
|
attendees: Some(meeting.attendees.clone()),
|
|
|
|
|
organizer: meeting.organizer.clone(),
|
|
|
|
|
reminder_minutes: meeting.reminder_minutes,
|
|
|
|
|
recurrence_rule: None,
|
|
|
|
|
}).await?;
|
|
|
|
|
|
|
|
|
|
// Create meeting record
|
|
|
|
|
let meeting_record = Meeting {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
event_id: event.id,
|
|
|
|
|
meeting_url: meeting.meeting_url,
|
|
|
|
|
meeting_id: meeting.meeting_id,
|
|
|
|
|
platform: meeting.platform.unwrap_or(MeetingPlatform::Internal),
|
|
|
|
|
recording_url: None,
|
|
|
|
|
notes: None,
|
|
|
|
|
action_items: vec![],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(meeting_record)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn set_reminder(&self, reminder: SetReminderRequest) -> Result<CalendarReminder, Box<dyn std::error::Error>> {
|
|
|
|
|
let reminder_record = CalendarReminder {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
event_id: reminder.event_id,
|
|
|
|
|
remind_at: reminder.remind_at,
|
|
|
|
|
message: reminder.message,
|
|
|
|
|
channel: reminder.channel,
|
|
|
|
|
sent: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(reminder_record)
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
|
|
|
|
|
async fn refresh_cache(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let results = sqlx::query!("SELECT * FROM calendar_events ORDER BY start_time ASC")
|
2025-11-26 22:54:22 -03:00
|
|
|
.load::<CalendarEvent>(&mut conn)?;
|
2025-11-22 22:55:35 -03:00
|
|
|
let events: Vec<CalendarEvent> = vec![];
|
|
|
|
|
let mut cache = self.cache.write().await;
|
|
|
|
|
*cache = events;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 09:38:50 -03:00
|
|
|
// Calendar API handlers
|
|
|
|
|
pub async fn handle_event_create(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Json(payload): Json<CreateEventRequest>,
|
|
|
|
|
) -> Result<Json<CalendarEvent>, StatusCode> {
|
|
|
|
|
let calendar = state.calendar_engine.as_ref()
|
|
|
|
|
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
|
|
|
|
|
|
match calendar.create_event(payload).await {
|
|
|
|
|
Ok(event) => Ok(Json(event)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to create event: {}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_event_update(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(payload): Json<UpdateEventRequest>,
|
|
|
|
|
) -> Result<Json<CalendarEvent>, StatusCode> {
|
|
|
|
|
let calendar = state.calendar_engine.as_ref()
|
|
|
|
|
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
|
|
|
|
|
|
match calendar.update_event(id, payload).await {
|
|
|
|
|
Ok(event) => Ok(Json(event)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to update event: {}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_event_delete(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
) -> Result<StatusCode, StatusCode> {
|
|
|
|
|
let calendar = state.calendar_engine.as_ref()
|
|
|
|
|
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
|
|
|
|
|
|
match calendar.delete_event(id).await {
|
|
|
|
|
Ok(_) => Ok(StatusCode::NO_CONTENT),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to delete event: {}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_events_list(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Query(query): Query<EventListQuery>,
|
|
|
|
|
) -> Result<Json<Vec<CalendarEvent>>, StatusCode> {
|
|
|
|
|
let calendar = state.calendar_engine.as_ref()
|
|
|
|
|
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
|
|
|
|
|
|
match calendar.list_events(query.start_date, query.end_date).await {
|
|
|
|
|
Ok(events) => Ok(Json(events)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to list events: {}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_events_search(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Query(query): Query<EventSearchQuery>,
|
|
|
|
|
) -> Result<Json<Vec<CalendarEvent>>, StatusCode> {
|
|
|
|
|
let calendar = state.calendar_engine.as_ref()
|
|
|
|
|
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
|
|
|
|
|
|
match calendar.search_events(&query.query).await {
|
|
|
|
|
Ok(events) => Ok(Json(events)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to search events: {}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_check_availability(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Query(query): Query<CheckAvailabilityQuery>,
|
|
|
|
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
|
|
|
|
let calendar = state.calendar_engine.as_ref()
|
|
|
|
|
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
|
|
|
|
|
|
match calendar.check_availability(query.start_time, query.end_time).await {
|
|
|
|
|
Ok(available) => Ok(Json(serde_json::json!({ "available": available }))),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to check availability: {}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_schedule_meeting(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Json(payload): Json<ScheduleMeetingRequest>,
|
|
|
|
|
) -> Result<Json<Meeting>, StatusCode> {
|
|
|
|
|
let calendar = state.calendar_engine.as_ref()
|
|
|
|
|
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
|
|
|
|
|
|
match calendar.schedule_meeting(payload).await {
|
|
|
|
|
Ok(meeting) => Ok(Json(meeting)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to schedule meeting: {}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_set_reminder(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Json(payload): Json<SetReminderRequest>,
|
|
|
|
|
) -> Result<Json<CalendarReminder>, StatusCode> {
|
|
|
|
|
let calendar = state.calendar_engine.as_ref()
|
|
|
|
|
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
|
|
|
|
|
|
match calendar.set_reminder(payload).await {
|
|
|
|
|
Ok(reminder) => Ok(Json(reminder)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to set reminder: {}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure calendar routes
|
|
|
|
|
pub fn configure_calendar_routes() -> Router<Arc<AppState>> {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/api/calendar/events", post(handle_event_create))
|
|
|
|
|
.route("/api/calendar/events", get(handle_events_list))
|
|
|
|
|
.route("/api/calendar/events/:id", put(handle_event_update))
|
|
|
|
|
.route("/api/calendar/events/:id", delete(handle_event_delete))
|
|
|
|
|
.route("/api/calendar/events/search", get(handle_events_search))
|
|
|
|
|
.route("/api/calendar/availability", get(handle_check_availability))
|
|
|
|
|
.route("/api/calendar/meetings", post(handle_schedule_meeting))
|
|
|
|
|
.route("/api/calendar/reminders", post(handle_set_reminder))
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct EventQuery {
|
|
|
|
|
pub start: Option<String>,
|
|
|
|
|
pub end: Option<String>,
|
|
|
|
|
pub user_id: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct MeetingRequest {
|
|
|
|
|
pub event_id: Uuid,
|
|
|
|
|
pub platform: MeetingPlatform,
|
2025-11-26 22:54:22 -03:00
|
|
|
|
|
|
|
|
/// Process due reminders
|
|
|
|
|
pub async fn process_reminders(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
|
|
|
|
let now = Utc::now();
|
|
|
|
|
let mut conn = self.db.get().map_err(|e| format!("DB connection error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
// Find events that need reminders sent
|
|
|
|
|
let events = diesel::sql_query(
|
|
|
|
|
"SELECT * FROM calendar_events
|
|
|
|
|
WHERE reminder_minutes IS NOT NULL
|
|
|
|
|
AND start_time - INTERVAL '1 minute' * reminder_minutes <= $1
|
|
|
|
|
AND start_time > $1
|
|
|
|
|
AND reminder_sent = false
|
|
|
|
|
ORDER BY start_time ASC"
|
|
|
|
|
)
|
|
|
|
|
.bind::<Timestamptz, _>(&now)
|
|
|
|
|
.load::<CalendarEvent>(&mut conn)?;
|
|
|
|
|
|
|
|
|
|
let mut notifications = Vec::new();
|
|
|
|
|
|
|
|
|
|
for event in events {
|
|
|
|
|
// Send reminder notification
|
|
|
|
|
let message = format!(
|
|
|
|
|
"Reminder: {} starting at {}",
|
|
|
|
|
event.title,
|
|
|
|
|
event.start_time.format("%H:%M")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Mark reminder as sent
|
|
|
|
|
diesel::sql_query(
|
|
|
|
|
"UPDATE calendar_events SET reminder_sent = true WHERE id = $1"
|
|
|
|
|
)
|
|
|
|
|
.bind::<diesel::sql_types::Uuid, _>(&event.id)
|
|
|
|
|
.execute(&mut conn)?;
|
|
|
|
|
|
|
|
|
|
notifications.push(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(notifications)
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
/// CalDAV Server implementation
|
|
|
|
|
pub mod caldav {
|
|
|
|
|
use super::*;
|
|
|
|
|
use axum::{
|
|
|
|
|
body::Body,
|
|
|
|
|
extract::{Path, State, Query},
|
|
|
|
|
http::{Method, StatusCode, header},
|
|
|
|
|
response::{Response, IntoResponse},
|
|
|
|
|
routing::{get, put, delete, any},
|
|
|
|
|
Router,
|
|
|
|
|
};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
pub fn create_caldav_router(calendar_engine: Arc<CalendarEngine>) -> Router {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/.well-known/caldav", get(caldav_redirect))
|
|
|
|
|
.route("/caldav/:user/", any(caldav_propfind))
|
|
|
|
|
.route("/caldav/:user/calendar/", any(caldav_calendar_handler))
|
|
|
|
|
.route("/caldav/:user/calendar/:event_uid.ics",
|
|
|
|
|
get(caldav_get_event)
|
|
|
|
|
.put(caldav_put_event)
|
|
|
|
|
.delete(caldav_delete_event))
|
|
|
|
|
.with_state(calendar_engine)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn caldav_redirect() -> impl IntoResponse {
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::MOVED_PERMANENTLY)
|
|
|
|
|
.header(header::LOCATION, "/caldav/")
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn caldav_propfind(
|
|
|
|
|
Path(user): Path<String>,
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let xml = format!(r#"<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
|
<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
|
|
|
|
<D:response>
|
|
|
|
|
<D:href>/caldav/{}/</D:href>
|
|
|
|
|
<D:propstat>
|
|
|
|
|
<D:prop>
|
|
|
|
|
<D:resourcetype>
|
|
|
|
|
<D:collection/>
|
|
|
|
|
<C:calendar/>
|
|
|
|
|
</D:resourcetype>
|
|
|
|
|
<D:displayname>{}'s Calendar</D:displayname>
|
|
|
|
|
<C:supported-calendar-component-set>
|
|
|
|
|
<C:comp name="VEVENT"/>
|
|
|
|
|
</C:supported-calendar-component-set>
|
|
|
|
|
</D:prop>
|
|
|
|
|
<D:status>HTTP/1.1 200 OK</D:status>
|
|
|
|
|
</D:propstat>
|
|
|
|
|
</D:response>
|
|
|
|
|
</D:multistatus>"#, user, user);
|
|
|
|
|
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from(xml))
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn caldav_calendar_handler(
|
|
|
|
|
Path(user): Path<String>,
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
method: Method,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
match method {
|
|
|
|
|
Method::GET => {
|
|
|
|
|
// Return calendar collection
|
|
|
|
|
let events = engine.get_user_events(&user).await.unwrap_or_default();
|
|
|
|
|
let ics = events_to_icalendar(&events, &user);
|
|
|
|
|
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, "text/calendar; charset=utf-8")
|
|
|
|
|
.body(Body::from(ics))
|
|
|
|
|
.unwrap()
|
|
|
|
|
},
|
|
|
|
|
_ => caldav_propfind(Path(user), State(engine)).await.into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn caldav_get_event(
|
|
|
|
|
Path((user, event_uid)): Path<(String, String)>,
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let event_id = event_uid.trim_end_matches(".ics");
|
|
|
|
|
|
|
|
|
|
match Uuid::parse_str(event_id) {
|
|
|
|
|
Ok(id) => {
|
|
|
|
|
match engine.get_event(id).await {
|
|
|
|
|
Ok(event) => {
|
|
|
|
|
let ics = event_to_icalendar(&event);
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, "text/calendar; charset=utf-8")
|
|
|
|
|
.body(Body::from(ics))
|
|
|
|
|
.unwrap()
|
|
|
|
|
},
|
|
|
|
|
Err(_) => Response::builder()
|
|
|
|
|
.status(StatusCode::NOT_FOUND)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap(),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(_) => Response::builder()
|
|
|
|
|
.status(StatusCode::BAD_REQUEST)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn caldav_put_event(
|
|
|
|
|
Path((user, event_uid)): Path<(String, String)>,
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
body: String,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
// Parse iCalendar data and create/update event
|
|
|
|
|
// This is a simplified implementation
|
|
|
|
|
StatusCode::CREATED
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn caldav_delete_event(
|
|
|
|
|
Path((user, event_uid)): Path<(String, String)>,
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let event_id = event_uid.trim_end_matches(".ics");
|
|
|
|
|
|
|
|
|
|
match Uuid::parse_str(event_id) {
|
|
|
|
|
Ok(id) => {
|
|
|
|
|
match engine.delete_event(id).await {
|
|
|
|
|
Ok(true) => StatusCode::NO_CONTENT,
|
|
|
|
|
Ok(false) => StatusCode::NOT_FOUND,
|
|
|
|
|
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(_) => StatusCode::BAD_REQUEST,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn events_to_icalendar(events: &[CalendarEvent], user: &str) -> String {
|
|
|
|
|
let mut ics = String::from("BEGIN:VCALENDAR\r\n");
|
|
|
|
|
ics.push_str("VERSION:2.0\r\n");
|
|
|
|
|
ics.push_str(&format!("PRODID:-//BotServer//Calendar {}//EN\r\n", user));
|
|
|
|
|
|
|
|
|
|
for event in events {
|
|
|
|
|
ics.push_str(&event_to_icalendar(event));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ics.push_str("END:VCALENDAR\r\n");
|
|
|
|
|
ics
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn event_to_icalendar(event: &CalendarEvent) -> String {
|
|
|
|
|
let mut vevent = String::from("BEGIN:VEVENT\r\n");
|
|
|
|
|
vevent.push_str(&format!("UID:{}\r\n", event.id));
|
|
|
|
|
vevent.push_str(&format!("SUMMARY:{}\r\n", event.title));
|
|
|
|
|
|
|
|
|
|
if let Some(desc) = &event.description {
|
|
|
|
|
vevent.push_str(&format!("DESCRIPTION:{}\r\n", desc));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(loc) = &event.location {
|
|
|
|
|
vevent.push_str(&format!("LOCATION:{}\r\n", loc));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vevent.push_str(&format!("DTSTART:{}\r\n", event.start_time.format("%Y%m%dT%H%M%SZ")));
|
|
|
|
|
vevent.push_str(&format!("DTEND:{}\r\n", event.end_time.format("%Y%m%dT%H%M%SZ")));
|
|
|
|
|
vevent.push_str(&format!("STATUS:{}\r\n", event.status.to_uppercase()));
|
|
|
|
|
|
|
|
|
|
for attendee in &event.attendees {
|
|
|
|
|
vevent.push_str(&format!("ATTENDEE:mailto:{}\r\n", attendee));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vevent.push_str("END:VEVENT\r\n");
|
|
|
|
|
vevent
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reminder job service
|
|
|
|
|
pub async fn start_reminder_job(engine: Arc<CalendarEngine>) {
|
|
|
|
|
use tokio::time::{interval, Duration};
|
|
|
|
|
|
|
|
|
|
let mut ticker = interval(Duration::from_secs(60)); // Check every minute
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
ticker.tick().await;
|
|
|
|
|
|
|
|
|
|
match engine.process_reminders().await {
|
|
|
|
|
Ok(notifications) => {
|
|
|
|
|
for message in notifications {
|
|
|
|
|
log::info!("Calendar reminder: {}", message);
|
|
|
|
|
// Here you would send actual notifications via email, push, etc.
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("Failed to process calendar reminders: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
async fn create_event_handler(
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
Json(event): Json<CalendarEvent>,
|
|
|
|
|
) -> Result<Json<CalendarEvent>, StatusCode> {
|
|
|
|
|
match engine.create_event(event).await {
|
|
|
|
|
Ok(created) => Ok(Json(created)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_events_handler(
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
Query(params): Query<EventQuery>,
|
|
|
|
|
) -> Result<Json<Vec<CalendarEvent>>, StatusCode> {
|
|
|
|
|
if let (Some(start), Some(end)) = (params.start, params.end) {
|
|
|
|
|
let start = DateTime::parse_from_rfc3339(&start)
|
|
|
|
|
.map(|dt| dt.with_timezone(&Utc))
|
|
|
|
|
.unwrap_or_else(|_| Utc::now());
|
|
|
|
|
let end = DateTime::parse_from_rfc3339(&end)
|
|
|
|
|
.map(|dt| dt.with_timezone(&Utc))
|
|
|
|
|
.unwrap_or_else(|_| Utc::now() + chrono::Duration::days(30));
|
|
|
|
|
|
|
|
|
|
match engine.get_events_range(start, end).await {
|
|
|
|
|
Ok(events) => Ok(Json(events)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
} else if let Some(user_id) = params.user_id {
|
|
|
|
|
match engine.get_user_events(&user_id).await {
|
|
|
|
|
Ok(events) => Ok(Json(events)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Err(StatusCode::BAD_REQUEST)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn update_event_handler(
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(updates): Json<serde_json::Value>,
|
|
|
|
|
) -> Result<Json<CalendarEvent>, StatusCode> {
|
|
|
|
|
match engine.update_event(id, updates).await {
|
|
|
|
|
Ok(updated) => Ok(Json(updated)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn delete_event_handler(
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
) -> Result<StatusCode, StatusCode> {
|
|
|
|
|
match engine.delete_event(id).await {
|
|
|
|
|
Ok(true) => Ok(StatusCode::NO_CONTENT),
|
|
|
|
|
Ok(false) => Err(StatusCode::NOT_FOUND),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn schedule_meeting_handler(
|
|
|
|
|
State(engine): State<Arc<CalendarEngine>>,
|
|
|
|
|
Json(req): Json<MeetingRequest>,
|
|
|
|
|
) -> Result<Json<Meeting>, StatusCode> {
|
|
|
|
|
match engine.create_meeting(req.event_id, req.platform).await {
|
|
|
|
|
Ok(meeting) => Ok(Json(meeting)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn routes(engine: Arc<CalendarEngine>) -> Router {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route(
|
|
|
|
|
"/events",
|
|
|
|
|
post(create_event_handler).get(get_events_handler),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/events/:id",
|
|
|
|
|
put(update_event_handler).delete(delete_event_handler),
|
|
|
|
|
)
|
|
|
|
|
.route("/meetings", post(schedule_meeting_handler))
|
|
|
|
|
.with_state(engine)
|
|
|
|
|
}
|