code icon Code

Connect to Google Calendar

Fetch calendar events from the last 90 days for profile analysis

Source Code

import fs from "fs";
import path from "path";

const [outputPath = "session/calendar-raw.json"] = process.argv.slice(2);

const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
const now = new Date();
const formatDate = (d) =>
  d.toLocaleDateString("en-US", { month: "short", day: "numeric" });

console.log("Collecting your calendar events (last 90 days)...");

try {
  // 1. Get user info and settings in parallel
  console.log("Fetching calendar data...");

  const [userInfo, calendars] = await Promise.all([
    // Get user's primary calendar info (includes timezone)
    (async () => {
      const res = await fetch(
        "https://www.googleapis.com/calendar/v3/calendars/primary",
        { headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
      );
      const data = await res.json();
      if (data.error) throw new Error(`Calendar info failed: ${data.error.message}`);
      return data;
    })(),

    // Get calendar list
    (async () => {
      const res = await fetch(
        "https://www.googleapis.com/calendar/v3/users/me/calendarList",
        { headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
      );
      const data = await res.json();
      if (data.error) throw new Error(`Calendar list failed: ${data.error.message}`);
      return data.items || [];
    })(),
  ]);

  const userEmail = userInfo.id;
  const userTimezone = userInfo.timeZone;
  const primaryCalendarId = userInfo.id;

  console.log(`✓ Connected as ${userEmail}`);
  console.log(`  Timezone: ${userTimezone}`);
  console.log(`  ${calendars.length} calendars found`);

  // 2. Fetch events from primary calendar
  console.log("Fetching events...");

  const events = [];
  let pageToken = null;

  do {
    const params = new URLSearchParams({
      timeMin: ninetyDaysAgo.toISOString(),
      timeMax: now.toISOString(),
      singleEvents: "true", // Expand recurring events
      orderBy: "startTime",
      maxResults: "250",
    });
    if (pageToken) params.set("pageToken", pageToken);

    const res = await fetch(
      `https://www.googleapis.com/calendar/v3/calendars/primary/events?${params}`,
      { headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
    );
    const data = await res.json();

    if (data.error) throw new Error(`Events fetch failed: ${data.error.message}`);

    events.push(...(data.items || []));
    pageToken = data.nextPageToken;
  } while (pageToken);

  console.log(`  Found ${events.length} events`);

  if (events.length === 0) {
    console.error("\n✗ No events found in the last 90 days.");
    console.log(
      JSON.stringify({
        success: false,
        error: "no_events_found",
        user: userEmail,
        timezone: userTimezone,
      })
    );
    process.exit(1);
  }

  // 3. Process events - extract relevant data for analysis
  const processedEvents = events.map((event) => {
    const isAllDay = !event.start?.dateTime;
    const startTime = event.start?.dateTime || event.start?.date;
    const endTime = event.end?.dateTime || event.end?.date;

    // Calculate duration in minutes (only for timed events)
    let durationMinutes = null;
    if (!isAllDay && startTime && endTime) {
      durationMinutes = Math.round(
        (new Date(endTime) - new Date(startTime)) / (1000 * 60)
      );
    }

    // Process attendees
    const attendees = (event.attendees || []).map((a) => ({
      email: a.email,
      displayName: a.displayName || a.email.split("@")[0],
      responseStatus: a.responseStatus, // needsAction, declined, tentative, accepted
      organizer: a.organizer || false,
      self: a.self || false,
    }));

    // Identify organizer
    const organizer = event.organizer
      ? {
          email: event.organizer.email,
          displayName: event.organizer.displayName || event.organizer.email?.split("@")[0],
          self: event.organizer.self || false,
        }
      : null;

    // Check for video conferencing
    const hasVideoConference = !!(
      event.conferenceData ||
      event.hangoutLink ||
      (event.location && /zoom|meet|teams/i.test(event.location))
    );

    return {
      id: event.id,
      title: event.summary || "(No title)",
      description: event.description || null,
      start: startTime,
      end: endTime,
      isAllDay,
      durationMinutes,
      timezone: event.start?.timeZone || userTimezone,
      location: event.location || null,
      hasVideoConference,
      organizer,
      attendees,
      attendeeCount: attendees.filter((a) => !a.self).length,
      recurringEventId: event.recurringEventId || null,
      isRecurring: !!event.recurringEventId,
      status: event.status, // confirmed, tentative, cancelled
      created: event.created,
      updated: event.updated,
    };
  });

  // Sort by start time (most recent first)
  processedEvents.sort((a, b) => new Date(b.start) - new Date(a.start));

  // 4. Build attendee summary for collaborator analysis
  const attendeeMap = new Map();

  for (const event of processedEvents) {
    // Skip all-day events for collaborator scoring (often not real meetings)
    if (event.isAllDay) continue;

    const isOneOnOne = event.attendeeCount === 1;

    for (const attendee of event.attendees) {
      if (attendee.self) continue; // Skip self

      if (!attendeeMap.has(attendee.email)) {
        attendeeMap.set(attendee.email, {
          email: attendee.email,
          displayName: attendee.displayName,
          oneOnOneCount: 0,
          groupMeetingCount: 0,
          organizedForMe: 0,
          iOrganizedFor: 0,
          recurringMeetings: 0,
          totalMeetings: 0,
        });
      }

      const stats = attendeeMap.get(attendee.email);
      stats.totalMeetings++;

      if (isOneOnOne) {
        stats.oneOnOneCount++;
      } else {
        stats.groupMeetingCount++;
      }

      if (event.isRecurring) {
        stats.recurringMeetings++;
      }

      // Track who organizes
      if (event.organizer) {
        if (event.organizer.self) {
          stats.iOrganizedFor++;
        } else if (event.organizer.email === attendee.email) {
          stats.organizedForMe++;
        }
      }
    }
  }

  // Convert to array and sort by engagement
  const collaborators = [...attendeeMap.values()]
    .map((c) => ({
      ...c,
      // Score similar to Slack's approach
      score:
        c.oneOnOneCount * 5 + // 1:1s are strongest signal
        c.recurringMeetings * 2 + // Recurring = consistent relationship
        c.organizedForMe * 2 + // They invite me
        c.iOrganizedFor * 2 + // I invite them
        c.groupMeetingCount * 1, // Group meetings weakest
    }))
    .sort((a, b) => b.score - a.score);

  // 5. Build summary statistics
  const timedEvents = processedEvents.filter((e) => !e.isAllDay);
  const allDayEvents = processedEvents.filter((e) => e.isAllDay);
  const recurringEvents = processedEvents.filter((e) => e.isRecurring);

  // Calculate events by day of week
  const dayOfWeekCounts = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
  for (const event of timedEvents) {
    const day = new Date(event.start).getDay();
    dayOfWeekCounts[day]++;
  }

  const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
  const eventsByDay = Object.entries(dayOfWeekCounts)
    .map(([day, count]) => ({ day: dayNames[day], count }))
    .sort((a, b) => b.count - a.count);

  // 6. Build output
  const output = {
    user: {
      email: userEmail,
      timezone: userTimezone,
    },
    period: `${formatDate(ninetyDaysAgo)} - ${formatDate(now)}`,
    summary: {
      totalEvents: processedEvents.length,
      timedEvents: timedEvents.length,
      allDayEvents: allDayEvents.length,
      recurringEvents: recurringEvents.length,
      uniqueCollaborators: collaborators.length,
      eventsByDayOfWeek: eventsByDay,
    },
    events: processedEvents,
    collaborators,
  };

  // Write output
  const dir = path.dirname(outputPath);
  if (dir && dir !== ".") fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  console.log(`\n✓ Collected ${processedEvents.length} events`);
  console.log(`  Timed meetings: ${timedEvents.length}`);
  console.log(`  All-day events: ${allDayEvents.length}`);
  console.log(`  Unique collaborators: ${collaborators.length}`);
  console.log(`  Output: ${outputPath}`);

  console.log(
    JSON.stringify({
      success: true,
      outputPath,
      user: userEmail,
      timezone: userTimezone,
      eventCount: processedEvents.length,
      collaboratorCount: collaborators.length,
    })
  );
} catch (error) {
  console.error("Failed:", error.message);
  throw error;
}