Skip to main content

mcp_gmailcal/
calendar_api.rs

1use crate::auth::TokenManager;
2use crate::config::Config;
3use chrono::{DateTime, Utc};
4use log::{debug, error};
5use reqwest::Client;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use tokio::sync::Mutex;
9use uuid::Uuid;
10
11const CALENDAR_API_BASE_URL: &str = "https://www.googleapis.com/calendar/v3";
12
13use crate::errors::{CalendarApiError, CalendarResult};
14
15// Alias for backward compatibility within this module
16type Result<T> = CalendarResult<T>;
17
18// Calendar event representation
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct CalendarEvent {
21    pub id: Option<String>,
22    pub summary: String,
23    pub description: Option<String>,
24    pub location: Option<String>,
25    pub start_time: DateTime<Utc>,
26    pub end_time: DateTime<Utc>,
27    pub attendees: Vec<Attendee>,
28    pub conference_data: Option<ConferenceData>,
29    pub html_link: Option<String>,
30    pub creator: Option<EventOrganizer>,
31    pub organizer: Option<EventOrganizer>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct EventOrganizer {
36    pub email: String,
37    pub display_name: Option<String>,
38    pub self_: Option<bool>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Attendee {
43    pub email: String,
44    pub display_name: Option<String>,
45    pub response_status: Option<String>,
46    pub optional: Option<bool>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ConferenceData {
51    pub conference_solution: Option<ConferenceSolution>,
52    pub entry_points: Vec<EntryPoint>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ConferenceSolution {
57    pub name: String,
58    pub key: Option<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct EntryPoint {
63    pub entry_point_type: String,
64    pub uri: String,
65    pub label: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CalendarList {
70    pub calendars: Vec<CalendarInfo>,
71    pub next_page_token: Option<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CalendarInfo {
76    pub id: String,
77    pub summary: String,
78    pub description: Option<String>,
79    pub primary: Option<bool>,
80}
81
82// Calendar API client
83#[derive(Debug, Clone)]
84pub struct CalendarClient {
85    client: Client,
86    token_manager: Arc<Mutex<TokenManager>>,
87}
88
89impl CalendarClient {
90    pub fn new(config: &Config) -> Self {
91        let client = Client::new();
92        // Reuse the Gmail token manager since they share the same OAuth scope
93        let token_manager = Arc::new(Mutex::new(TokenManager::new(config)));
94
95        Self {
96            client,
97            token_manager,
98        }
99    }
100
101    // Get a list of all calendars
102    pub async fn list_calendars(&self) -> Result<CalendarList> {
103        let token = self
104            .token_manager
105            .lock()
106            .await
107            .get_token(&self.client)
108            .await
109            .map_err(|e| CalendarApiError::AuthError(e.to_string()))?;
110
111        let url = format!("{}/users/me/calendarList", CALENDAR_API_BASE_URL);
112        debug!("Listing calendars from: {}", url);
113
114        let response = self
115            .client
116            .get(&url)
117            .header("Authorization", format!("Bearer {}", token))
118            .send()
119            .await
120            .map_err(|e| CalendarApiError::NetworkError(e.to_string()))?;
121
122        let status = response.status();
123        if !status.is_success() {
124            let error_text = response
125                .text()
126                .await
127                .unwrap_or_else(|_| "<no response body>".to_string());
128            return Err(CalendarApiError::ApiError(format!(
129                "Failed to list calendars. Status: {}, Error: {}",
130                status, error_text
131            )));
132        }
133
134        let json_response = response
135            .json::<serde_json::Value>()
136            .await
137            .map_err(|e| CalendarApiError::ParseError(e.to_string()))?;
138
139        let mut calendars = Vec::new();
140
141        if let Some(items) = json_response.get("items").and_then(|v| v.as_array()) {
142            for item in items {
143                let id = item
144                    .get("id")
145                    .and_then(|v| v.as_str())
146                    .ok_or_else(|| CalendarApiError::ParseError("Missing calendar id".to_string()))?
147                    .to_string();
148
149                let summary = item
150                    .get("summary")
151                    .and_then(|v| v.as_str())
152                    .unwrap_or("Unknown Calendar")
153                    .to_string();
154
155                let description = item
156                    .get("description")
157                    .and_then(|v| v.as_str())
158                    .map(|s| s.to_string());
159
160                let primary = item.get("primary").and_then(|v| v.as_bool());
161
162                calendars.push(CalendarInfo {
163                    id,
164                    summary,
165                    description,
166                    primary,
167                });
168            }
169        }
170
171        let next_page_token = json_response
172            .get("nextPageToken")
173            .and_then(|v| v.as_str())
174            .map(|s| s.to_string());
175
176        Ok(CalendarList {
177            calendars,
178            next_page_token,
179        })
180    }
181
182    // Get events from a specific calendar
183    pub async fn list_events(
184        &self,
185        calendar_id: &str,
186        max_results: Option<u32>,
187        time_min: Option<DateTime<Utc>>,
188        time_max: Option<DateTime<Utc>>,
189    ) -> Result<Vec<CalendarEvent>> {
190        let token = self
191            .token_manager
192            .lock()
193            .await
194            .get_token(&self.client)
195            .await
196            .map_err(|e| CalendarApiError::AuthError(e.to_string()))?;
197
198        let mut url = format!("{}/calendars/{}/events", CALENDAR_API_BASE_URL, calendar_id);
199
200        // Build query parameters
201        let mut query_parts = Vec::new();
202
203        if let Some(max) = max_results {
204            query_parts.push(format!("maxResults={}", max));
205        }
206
207        if let Some(min_time) = time_min {
208            let encoded_time = urlencoding::encode(&min_time.to_rfc3339()).into_owned();
209            query_parts.push(format!("timeMin={}", encoded_time));
210        }
211
212        if let Some(max_time) = time_max {
213            let encoded_time = urlencoding::encode(&max_time.to_rfc3339()).into_owned();
214            query_parts.push(format!("timeMax={}", encoded_time));
215        }
216
217        // Add single events mode to expand recurring events
218        query_parts.push("singleEvents=true".to_string());
219
220        // Order by start time
221        query_parts.push("orderBy=startTime".to_string());
222
223        if !query_parts.is_empty() {
224            url = format!("{}?{}", url, query_parts.join("&"));
225        }
226
227        debug!("Listing events from: {}", url);
228
229        let response = self
230            .client
231            .get(&url)
232            .header("Authorization", format!("Bearer {}", token))
233            .send()
234            .await
235            .map_err(|e| CalendarApiError::NetworkError(e.to_string()))?;
236
237        let status = response.status();
238        if !status.is_success() {
239            let error_text = response
240                .text()
241                .await
242                .unwrap_or_else(|_| "<no response body>".to_string());
243            return Err(CalendarApiError::ApiError(format!(
244                "Failed to list events. Status: {}, Error: {}",
245                status, error_text
246            )));
247        }
248
249        let json_response = response
250            .json::<serde_json::Value>()
251            .await
252            .map_err(|e| CalendarApiError::ParseError(e.to_string()))?;
253
254        let mut events = Vec::new();
255
256        if let Some(items) = json_response.get("items").and_then(|v| v.as_array()) {
257            for item in items {
258                if let Ok(event) = self.parse_event(item) {
259                    events.push(event);
260                } else {
261                    // Log parsing error but continue with other events
262                    error!("Failed to parse event: {:?}", item);
263                }
264            }
265        }
266
267        Ok(events)
268    }
269
270    // Create a new calendar event
271    pub async fn create_event(
272        &self,
273        calendar_id: &str,
274        event: CalendarEvent,
275    ) -> Result<CalendarEvent> {
276        let token = self
277            .token_manager
278            .lock()
279            .await
280            .get_token(&self.client)
281            .await
282            .map_err(|e| CalendarApiError::AuthError(e.to_string()))?;
283
284        let url = format!("{}/calendars/{}/events", CALENDAR_API_BASE_URL, calendar_id);
285        debug!("Creating new event in calendar {}", calendar_id);
286
287        // Convert our CalendarEvent to Google Calendar API format
288        let mut event_data = serde_json::Map::new();
289        event_data.insert(
290            "summary".to_string(),
291            serde_json::Value::String(event.summary),
292        );
293
294        if let Some(desc) = event.description {
295            event_data.insert("description".to_string(), serde_json::Value::String(desc));
296        }
297
298        if let Some(loc) = event.location {
299            event_data.insert("location".to_string(), serde_json::Value::String(loc));
300        }
301
302        // Add start time
303        let mut start = serde_json::Map::new();
304        start.insert(
305            "dateTime".to_string(),
306            serde_json::Value::String(event.start_time.to_rfc3339()),
307        );
308        start.insert(
309            "timeZone".to_string(),
310            serde_json::Value::String("UTC".to_string()),
311        );
312        event_data.insert("start".to_string(), serde_json::Value::Object(start));
313
314        // Add end time
315        let mut end = serde_json::Map::new();
316        end.insert(
317            "dateTime".to_string(),
318            serde_json::Value::String(event.end_time.to_rfc3339()),
319        );
320        end.insert(
321            "timeZone".to_string(),
322            serde_json::Value::String("UTC".to_string()),
323        );
324        event_data.insert("end".to_string(), serde_json::Value::Object(end));
325
326        // Add attendees if any
327        if !event.attendees.is_empty() {
328            let attendees = event
329                .attendees
330                .iter()
331                .map(|a| {
332                    let mut attendee = serde_json::Map::new();
333                    attendee.insert(
334                        "email".to_string(),
335                        serde_json::Value::String(a.email.clone()),
336                    );
337
338                    if let Some(name) = &a.display_name {
339                        attendee.insert(
340                            "displayName".to_string(),
341                            serde_json::Value::String(name.clone()),
342                        );
343                    }
344
345                    if let Some(status) = &a.response_status {
346                        attendee.insert(
347                            "responseStatus".to_string(),
348                            serde_json::Value::String(status.clone()),
349                        );
350                    }
351
352                    if let Some(optional) = a.optional {
353                        attendee.insert("optional".to_string(), serde_json::Value::Bool(optional));
354                    }
355
356                    serde_json::Value::Object(attendee)
357                })
358                .collect::<Vec<_>>();
359
360            event_data.insert("attendees".to_string(), serde_json::Value::Array(attendees));
361        }
362
363        // Generate unique ID for request for idempotency
364        // This header ensures the request can be safely retried without creating duplicate events
365        // Google recommends using the same ID for retries of the same logical operation
366        let request_id = Uuid::new_v4().to_string();
367        debug!("Using idempotency header X-Goog-Request-ID: {}", request_id);
368
369        // Store the request ID for potential retry operations
370        // This would typically be stored in a transaction log or retry mechanism
371
372        let response = self
373            .client
374            .post(&url)
375            .header("Authorization", format!("Bearer {}", token))
376            .header("Content-Type", "application/json")
377            // Add idempotency header to prevent duplicate events on retry
378            .header("X-Goog-Request-ID", request_id)
379            .json(&event_data)
380            .send()
381            .await
382            .map_err(|e| CalendarApiError::NetworkError(e.to_string()))?;
383
384        let status = response.status();
385        if !status.is_success() {
386            let error_text = response
387                .text()
388                .await
389                .unwrap_or_else(|_| "<no response body>".to_string());
390            return Err(CalendarApiError::ApiError(format!(
391                "Failed to create event. Status: {}, Error: {}",
392                status, error_text
393            )));
394        }
395
396        let json_response = response
397            .json::<serde_json::Value>()
398            .await
399            .map_err(|e| CalendarApiError::ParseError(e.to_string()))?;
400
401        self.parse_event(&json_response)
402    }
403
404    // Get a specific event
405    pub async fn get_event(&self, calendar_id: &str, event_id: &str) -> Result<CalendarEvent> {
406        let token = self
407            .token_manager
408            .lock()
409            .await
410            .get_token(&self.client)
411            .await
412            .map_err(|e| CalendarApiError::AuthError(e.to_string()))?;
413
414        let url = format!(
415            "{}/calendars/{}/events/{}",
416            CALENDAR_API_BASE_URL, calendar_id, event_id
417        );
418        debug!("Getting event {} from calendar {}", event_id, calendar_id);
419
420        let response = self
421            .client
422            .get(&url)
423            .header("Authorization", format!("Bearer {}", token))
424            .send()
425            .await
426            .map_err(|e| CalendarApiError::NetworkError(e.to_string()))?;
427
428        let status = response.status();
429        if !status.is_success() {
430            let error_text = response
431                .text()
432                .await
433                .unwrap_or_else(|_| "<no response body>".to_string());
434            return Err(CalendarApiError::ApiError(format!(
435                "Failed to get event. Status: {}, Error: {}",
436                status, error_text
437            )));
438        }
439
440        let json_response = response
441            .json::<serde_json::Value>()
442            .await
443            .map_err(|e| CalendarApiError::ParseError(e.to_string()))?;
444
445        self.parse_event(&json_response)
446    }
447
448    // Helper to parse Google Calendar event format into our CalendarEvent struct
449    fn parse_event(&self, item: &serde_json::Value) -> Result<CalendarEvent> {
450        let id = item
451            .get("id")
452            .and_then(|v| v.as_str())
453            .map(|s| s.to_string());
454
455        let summary = item
456            .get("summary")
457            .and_then(|v| v.as_str())
458            .ok_or_else(|| CalendarApiError::ParseError("Missing event summary".to_string()))?
459            .to_string();
460
461        let description = item
462            .get("description")
463            .and_then(|v| v.as_str())
464            .map(|s| s.to_string());
465
466        let location = item
467            .get("location")
468            .and_then(|v| v.as_str())
469            .map(|s| s.to_string());
470
471        // Parse datetime structures
472        let start_time = item
473            .get("start")
474            .and_then(|v| v.get("dateTime"))
475            .and_then(|v| v.as_str())
476            .ok_or_else(|| CalendarApiError::ParseError("Missing start time".to_string()))?;
477
478        let end_time = item
479            .get("end")
480            .and_then(|v| v.get("dateTime"))
481            .and_then(|v| v.as_str())
482            .ok_or_else(|| CalendarApiError::ParseError("Missing end time".to_string()))?;
483
484        // Parse RFC3339 format to DateTime<Utc>
485        let start_dt = DateTime::parse_from_rfc3339(start_time)
486            .map_err(|e| CalendarApiError::ParseError(format!("Invalid start time: {}", e)))?
487            .with_timezone(&Utc);
488
489        let end_dt = DateTime::parse_from_rfc3339(end_time)
490            .map_err(|e| CalendarApiError::ParseError(format!("Invalid end time: {}", e)))?
491            .with_timezone(&Utc);
492
493        // Parse attendees
494        let mut attendees = Vec::new();
495        if let Some(attendee_list) = item.get("attendees").and_then(|v| v.as_array()) {
496            for attendee in attendee_list {
497                let email = attendee
498                    .get("email")
499                    .and_then(|v| v.as_str())
500                    .ok_or_else(|| {
501                        CalendarApiError::ParseError("Missing attendee email".to_string())
502                    })?
503                    .to_string();
504
505                let display_name = attendee
506                    .get("displayName")
507                    .and_then(|v| v.as_str())
508                    .map(|s| s.to_string());
509
510                let response_status = attendee
511                    .get("responseStatus")
512                    .and_then(|v| v.as_str())
513                    .map(|s| s.to_string());
514
515                let optional = attendee.get("optional").and_then(|v| v.as_bool());
516
517                attendees.push(Attendee {
518                    email,
519                    display_name,
520                    response_status,
521                    optional,
522                });
523            }
524        }
525
526        // Parse conference data
527        let conference_data = if let Some(conf_data) = item.get("conferenceData") {
528            let mut entry_points = Vec::new();
529
530            if let Some(entry_point_list) = conf_data.get("entryPoints").and_then(|v| v.as_array())
531            {
532                for entry_point in entry_point_list {
533                    if let (Some(entry_type), Some(uri)) = (
534                        entry_point.get("entryPointType").and_then(|v| v.as_str()),
535                        entry_point.get("uri").and_then(|v| v.as_str()),
536                    ) {
537                        entry_points.push(EntryPoint {
538                            entry_point_type: entry_type.to_string(),
539                            uri: uri.to_string(),
540                            label: entry_point
541                                .get("label")
542                                .and_then(|v| v.as_str())
543                                .map(|s| s.to_string()),
544                        });
545                    }
546                }
547            }
548
549            let conference_solution = conf_data.get("conferenceSolution").and_then(|sol| {
550                sol.get("name")
551                    .and_then(|v| v.as_str())
552                    .map(|name| ConferenceSolution {
553                        name: name.to_string(),
554                        key: sol
555                            .get("key")
556                            .and_then(|v| v.as_str())
557                            .map(|s| s.to_string()),
558                    })
559            });
560
561            if !entry_points.is_empty() || conference_solution.is_some() {
562                Some(ConferenceData {
563                    conference_solution,
564                    entry_points,
565                })
566            } else {
567                None
568            }
569        } else {
570            None
571        };
572
573        // Parse html link
574        let html_link = item
575            .get("htmlLink")
576            .and_then(|v| v.as_str())
577            .map(|s| s.to_string());
578
579        // Parse creator
580        let creator = item.get("creator").and_then(|c| {
581            c.get("email")
582                .and_then(|v| v.as_str())
583                .map(|email| EventOrganizer {
584                    email: email.to_string(),
585                    display_name: c
586                        .get("displayName")
587                        .and_then(|v| v.as_str())
588                        .map(|s| s.to_string()),
589                    self_: c.get("self").and_then(|v| v.as_bool()),
590                })
591        });
592
593        // Parse organizer
594        let organizer = item.get("organizer").and_then(|o| {
595            o.get("email")
596                .and_then(|v| v.as_str())
597                .map(|email| EventOrganizer {
598                    email: email.to_string(),
599                    display_name: o
600                        .get("displayName")
601                        .and_then(|v| v.as_str())
602                        .map(|s| s.to_string()),
603                    self_: o.get("self").and_then(|v| v.as_bool()),
604                })
605        });
606
607        Ok(CalendarEvent {
608            id,
609            summary,
610            description,
611            location,
612            start_time: start_dt,
613            end_time: end_dt,
614            attendees,
615            conference_data,
616            html_link,
617            creator,
618            organizer,
619        })
620    }
621}