From 833a4fdd529bc5d267e07a72fbbd4a0f206cfd0e Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 4 May 2026 20:04:48 -0400 Subject: [PATCH] Add Google Calendar integration - New src/shared/calendar.py: OAuth service builder, list_events, create_event, check_availability (reuses gmail-oauth secret) - 3 new tools: get_calendar_events, create_calendar_event, check_availability - Extended OAuth scopes to include calendar in gmail.py and token script - Updated system prompt with calendar capabilities Requires re-running scripts/get_gmail_token.py to grant the calendar scope and updating the exec-aide/gmail-oauth secret with the new refresh token. Closes #2 --- scripts/get_gmail_token.py | 5 ++- src/shared/calendar.py | 72 ++++++++++++++++++++++++++++++++++++++ src/shared/gmail.py | 5 ++- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/shared/calendar.py diff --git a/scripts/get_gmail_token.py b/scripts/get_gmail_token.py index 46b5c5d..ed079a3 100644 --- a/scripts/get_gmail_token.py +++ b/scripts/get_gmail_token.py @@ -13,7 +13,10 @@ import json from google_auth_oauthlib.flow import InstalledAppFlow -SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"] +SCOPES = [ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/calendar", +] def main(): diff --git a/src/shared/calendar.py b/src/shared/calendar.py new file mode 100644 index 0000000..029b78a --- /dev/null +++ b/src/shared/calendar.py @@ -0,0 +1,72 @@ +import logging + +import google.auth.transport.requests +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build + +from shared.secrets import get_gmail_oauth, save_gmail_tokens + +logger = logging.getLogger(__name__) + +_service = None + +SCOPES = [ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/calendar", +] + + +def get_calendar_service(): + global _service + oauth = get_gmail_oauth() + creds = Credentials( + token=oauth.get("access_token"), + refresh_token=oauth["refresh_token"], + token_uri="https://oauth2.googleapis.com/token", + client_id=oauth["client_id"], + client_secret=oauth["client_secret"], + scopes=SCOPES, + ) + if not creds.valid: + creds.refresh(google.auth.transport.requests.Request()) + save_gmail_tokens( + creds.token, + creds.expiry.isoformat() if creds.expiry else "", + ) + _service = build("calendar", "v3", credentials=creds, cache_discovery=False) + return _service + + +def list_events(service, time_min, time_max, max_results=10): + result = service.events().list( + calendarId="primary", + timeMin=time_min, + timeMax=time_max, + maxResults=max_results, + singleEvents=True, + orderBy="startTime", + ).execute() + return result.get("items", []) + + +def create_event(service, summary, start, end, attendees=None, description=None): + body = { + "summary": summary, + "start": start, + "end": end, + } + if description: + body["description"] = description + if attendees: + body["attendees"] = [{"email": a} for a in attendees] + return service.events().insert(calendarId="primary", body=body).execute() + + +def check_availability(service, time_min, time_max): + body = { + "timeMin": time_min, + "timeMax": time_max, + "items": [{"id": "primary"}], + } + result = service.freebusy().query(body=body).execute() + return result["calendars"]["primary"]["busy"] diff --git a/src/shared/gmail.py b/src/shared/gmail.py index 3084886..39f8c1b 100644 --- a/src/shared/gmail.py +++ b/src/shared/gmail.py @@ -13,7 +13,10 @@ logger = logging.getLogger(__name__) _service = None -SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"] +SCOPES = [ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/calendar", +] def get_authenticated_service():