google-calendar.ts•7.18 kB
import { authenticate } from "@google-cloud/local-auth";
import type { Credentials, OAuth2Client } from "google-auth-library";
import { google } from "googleapis";
import * as fs from "node:fs";
import * as path from "node:path";
import { OAuth2Client as OAuth2 } from "google-auth-library";
import type {
	CalendarEvent,
	GoogleCalendarEvent,
	GoogleCalendarEventList,
	GoogleCalendarList,
} from "../types/index.js";
interface GoogleCredentials {
	installed?: {
		client_id: string;
		client_secret: string;
		redirect_uris: string[];
	};
	web?: {
		client_id: string;
		client_secret: string;
		redirect_uris: string[];
	};
}
/**
 * Service for interacting with Google Calendar API
 */
export class GoogleCalendarService {
	private static instance: GoogleCalendarService;
	private authClient: OAuth2Client | null = null;
	private credentials: Credentials | null = null;
	private readonly CREDENTIALS_PATH =
		process.env.CREDENTIALS_PATH ?? "./credentials.json";
	private readonly TOKEN_PATH = path.join(
		path.dirname(this.CREDENTIALS_PATH),
		"mcp-google-calendar-token.json",
	);
	// Scopes for Google Calendar API
	private readonly SCOPES = [
		"https://www.googleapis.com/auth/calendar.readonly",
		"https://www.googleapis.com/auth/calendar.events",
	];
	private constructor() {}
	/**
	 * Get the singleton instance of GoogleCalendarService
	 */
	public static getInstance(): GoogleCalendarService {
		if (!GoogleCalendarService.instance) {
			GoogleCalendarService.instance = new GoogleCalendarService();
		}
		return GoogleCalendarService.instance;
	}
	/**
	 * Load credentials from environment variable
	 */
	private loadCredentials(): GoogleCredentials {
		if (!this.CREDENTIALS_PATH) {
			throw new Error("CREDENTIALS_PATH environment variable is not set");
		}
		if (!fs.existsSync(this.CREDENTIALS_PATH)) {
			throw new Error(`Credentials file not found at ${this.CREDENTIALS_PATH}`);
		}
		const content = fs.readFileSync(this.CREDENTIALS_PATH, "utf8");
		return JSON.parse(content) as GoogleCredentials;
	}
	/**
	 * Load saved token from disk
	 */
	private loadSavedToken(): Credentials | null {
		try {
			if (fs.existsSync(this.TOKEN_PATH)) {
				const token = fs.readFileSync(this.TOKEN_PATH, "utf8");
				return JSON.parse(token);
			}
		} catch (error) {
			console.error("Error loading saved token:", error);
		}
		return null;
	}
	/**
	 * Save token to disk
	 */
	private saveToken(token: Credentials): void {
		try {
			fs.writeFileSync(this.TOKEN_PATH, JSON.stringify(token));
		} catch (error) {
			console.error("Error saving token:", error);
		}
	}
	/**
	 * Authorize with Google Calendar API
	 */
	public async authorize(): Promise<OAuth2Client> {
		// If we already have an authorized client, return it
		if (this.authClient && this.credentials) {
			this.authClient.setCredentials(this.credentials);
			return this.authClient;
		}
		try {
			// Load credentials from environment variable
			if (!fs.existsSync(this.CREDENTIALS_PATH)) {
				throw new Error(
					`Credentials file not found at: ${this.CREDENTIALS_PATH}`,
				);
			}
			// Load credentials from file
			const credentials: GoogleCredentials = JSON.parse(
				fs.readFileSync(this.CREDENTIALS_PATH, "utf-8"),
			);
			// Extract client ID and secret
			const clientId =
				credentials.web?.client_id || credentials.installed?.client_id;
			const clientSecret =
				credentials.web?.client_secret || credentials.installed?.client_secret;
			const redirectUri = (credentials.web?.redirect_uris ||
				credentials.installed?.redirect_uris ||
				[])[0];
			if (!clientId || !clientSecret) {
				throw new Error("Invalid credentials file format");
			}
			// Create OAuth2 client
			const authClient = new OAuth2(clientId, clientSecret, redirectUri);
			this.authClient = authClient;
			// Try to load saved token
			const savedToken = this.loadSavedToken();
			if (savedToken) {
				this.credentials = savedToken;
				this.authClient.setCredentials(savedToken);
				return this.authClient;
			}
			// If no saved token, start new authentication flow
			this.authClient = (await authenticate({
				scopes: this.SCOPES,
				keyfilePath: this.CREDENTIALS_PATH,
			})) as OAuth2Client;
			// Store credentials in memory and save to disk
			this.credentials = this.authClient.credentials;
			this.saveToken(this.credentials);
			return this.authClient;
		} catch (error) {
			console.error("Error authorizing with Google:", error);
			throw error;
		}
	}
	/**
	 * Get list of available calendars
	 */
	public async getCalendars(pageToken?: string): Promise<GoogleCalendarList> {
		const auth = await this.authorize();
		const calendar = google.calendar({ version: "v3", auth });
		const res = await calendar.calendarList.list({
			pageToken,
		});
		return res.data;
	}
	/**
	 * Get calendar events between two dates
	 */
	public async getEvents(
		calendarId: string,
		startDate: string,
		endDate: string,
		pageToken?: string,
	): Promise<GoogleCalendarEventList> {
		const auth = await this.authorize();
		const calendar = google.calendar({ version: "v3", auth });
		if (startDate === endDate) {
			endDate = endDate + "T23:59:59";
		}
		const res = await calendar.events.list({
			calendarId,
			timeMin: new Date(startDate).toISOString(),
			timeMax: new Date(endDate).toISOString(),
			singleEvents: true,
			orderBy: "startTime",
			pageToken,
		});
		return res.data;
	}
	/**
	 * Create a new calendar event
	 */
	public async createEvent(
		calendarId: string,
		event: CalendarEvent,
	): Promise<GoogleCalendarEvent> {
		const auth = await this.authorize();
		const calendar = google.calendar({ version: "v3", auth });
		const res = await calendar.events.insert({
			calendarId,
			requestBody: {
				summary: event.summary,
				description: event.description,
				start: { date: event.start },
				end: { date: event.end },
				anyoneCanAddSelf: event.anyoneCanAddSelf,
				colorId: event.colorId,
			},
		});
		return res.data;
	}
	/**
	 * Get a calendar event
	 */
	public async getEvent(
		calendarId: string,
		eventId: string,
	): Promise<GoogleCalendarEvent> {
		const auth = await this.authorize();
		const calendar = google.calendar({ version: "v3", auth });
		const res = await calendar.events.get({
			calendarId,
			eventId,
		});
		return res.data;
	}
	/**
	 * Edit a calendar event
	 */
	public async updateEvent(
		calendarId: string,
		eventId: string,
		event: CalendarEvent,
	): Promise<GoogleCalendarEvent> {
		const auth = await this.authorize();
		const calendar = google.calendar({ version: "v3", auth });
		const res = await calendar.events.update({
			calendarId,
			eventId,
			requestBody: {
				summary: event.summary,
				description: event.description,
				start: { date: event.start },
				end: { date: event.end },
				anyoneCanAddSelf: event.anyoneCanAddSelf,
				colorId: event.colorId,
			},
		});
		return res.data;
	}
	/**
	 * Delete a calendar event
	 */
	public async deleteEvent(calendarId: string, eventId: string): Promise<void> {
		const auth = await this.authorize();
		const calendar = google.calendar({ version: "v3", auth });
		await calendar.events.delete({
			calendarId,
			eventId,
		});
	}
}