22import type { calendar_v3 } from "@googleapis/calendar" ;
33import type { GaxiosResponse } from "googleapis-common" ;
44import { RRule } from "rrule" ;
5+ import { v4 as uuid } from "uuid" ;
56
67import { MeetLocationType } from "@calcom/app-store/constants" ;
78import { getLocation , getRichDescription } from "@calcom/lib/CalEventParser" ;
9+ import { uniqueBy } from "@calcom/lib/array" ;
810import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants" ;
911import logger from "@calcom/lib/logger" ;
1012import { safeStringify } from "@calcom/lib/safeStringify" ;
@@ -32,6 +34,12 @@ interface GoogleCalError extends Error {
3234 code ?: number ;
3335}
3436
37+ const MS_PER_DAY = 24 * 60 * 60 * 1000 ;
38+ const ONE_MONTH_IN_MS = 30 * MS_PER_DAY ;
39+
40+ const GOOGLE_WEBHOOK_URL_BASE = process . env . GOOGLE_WEBHOOK_URL || process . env . NEXT_PUBLIC_WEBAPP_URL ;
41+ const GOOGLE_WEBHOOK_URL = `${ GOOGLE_WEBHOOK_URL_BASE } /api/integrations/googlecalendar/webhook` ;
42+
3543const isGaxiosResponse = ( error : unknown ) : error is GaxiosResponse < calendar_v3 . Schema$Event > =>
3644 typeof error === "object" && ! ! error && Object . prototype . hasOwnProperty . call ( error , "config" ) ;
3745
@@ -112,6 +120,50 @@ export default class GoogleCalendarService implements Calendar {
112120 return attendees ;
113121 } ;
114122
123+ private async stopWatchingCalendarsInGoogle (
124+ channels : { googleChannelResourceId : string | null ; googleChannelId : string | null } [ ]
125+ ) {
126+ const calendar = await this . authedCalendar ( ) ;
127+ logger . debug ( `Unsubscribing from calendars ${ channels . map ( ( c ) => c . googleChannelId ) . join ( ", " ) } ` ) ;
128+ const uniqueChannels = uniqueBy ( channels , [ "googleChannelId" , "googleChannelResourceId" ] ) ;
129+ await Promise . allSettled (
130+ uniqueChannels . map ( ( { googleChannelResourceId, googleChannelId } ) =>
131+ calendar . channels
132+ . stop ( {
133+ requestBody : {
134+ resourceId : googleChannelResourceId ,
135+ id : googleChannelId ,
136+ } ,
137+ } )
138+ . catch ( ( err ) => {
139+ console . warn ( JSON . stringify ( err ) ) ;
140+ } )
141+ )
142+ ) ;
143+ }
144+
145+ private async startWatchingCalendarsInGoogle ( { calendarId } : { calendarId : string } ) {
146+ const calendar = await this . authedCalendar ( ) ;
147+ logger . debug ( `Subscribing to calendar ${ calendarId } ` , safeStringify ( { GOOGLE_WEBHOOK_URL } ) ) ;
148+
149+ const res = await calendar . events . watch ( {
150+ // Calendar identifier. To retrieve calendar IDs call the calendarList.list method. If you want to access the primary calendar of the currently logged in user, use the "primary" keyword.
151+ calendarId,
152+ requestBody : {
153+ // A UUID or similar unique string that identifies this channel.
154+ id : uuid ( ) ,
155+ type : "web_hook" ,
156+ address : GOOGLE_WEBHOOK_URL ,
157+ token : process . env . GOOGLE_WEBHOOK_TOKEN ,
158+ params : {
159+ // The time-to-live in seconds for the notification channel. Default is 604800 seconds.
160+ ttl : `${ Math . round ( ONE_MONTH_IN_MS / 1000 ) } ` ,
161+ } ,
162+ } ,
163+ } ) ;
164+ return res . data ;
165+ }
166+
115167 async createEvent (
116168 calEvent : CalendarServiceEvent ,
117169 credentialId : number ,
@@ -400,7 +452,9 @@ export default class GoogleCalendarService implements Calendar {
400452 return apiResponse . json ;
401453 }
402454
403- async getFreeBusyResult ( args : FreeBusyArgs ) : Promise < calendar_v3 . Schema$FreeBusyResponse > {
455+ async getFreeBusyResult (
456+ args : FreeBusyArgs ,
457+ ) : Promise < calendar_v3 . Schema$FreeBusyResponse > {
404458 return await this . fetchAvailability ( args ) ;
405459 }
406460
@@ -417,7 +471,9 @@ export default class GoogleCalendarService implements Calendar {
417471 return validCals [ 0 ] ;
418472 }
419473
420- async getFreeBusyData ( args : FreeBusyArgs ) : Promise < ( EventBusyDate & { id : string } ) [ ] | null > {
474+ async getFreeBusyData (
475+ args : FreeBusyArgs ,
476+ ) : Promise < ( EventBusyDate & { id : string } ) [ ] | null > {
421477 const freeBusyResult = await this . getFreeBusyResult ( args ) ;
422478 if ( ! freeBusyResult . calendars ) return null ;
423479
@@ -548,12 +604,11 @@ export default class GoogleCalendarService implements Calendar {
548604
549605 /**
550606 * Fetches availability data using the cache-or-fetch pattern
551- *
552607 */
553608 private async fetchAvailabilityData (
554609 calendarIds : string [ ] ,
555610 dateFrom : string ,
556- dateTo : string
611+ dateTo : string ,
557612 ) : Promise < EventBusyDate [ ] > {
558613 // More efficient date difference calculation using native Date objects
559614 // Use Math.floor to match dayjs diff behavior (truncates, doesn't round up)
@@ -564,11 +619,13 @@ export default class GoogleCalendarService implements Calendar {
564619
565620 // Google API only allows a date range of 90 days for /freebusy
566621 if ( diff <= 90 ) {
567- const freeBusyData = await this . getFreeBusyData ( {
568- timeMin : dateFrom ,
569- timeMax : dateTo ,
570- items : calendarIds . map ( ( id ) => ( { id } ) ) ,
571- } ) ;
622+ const freeBusyData = await this . getFreeBusyData (
623+ {
624+ timeMin : dateFrom ,
625+ timeMax : dateTo ,
626+ items : calendarIds . map ( ( id ) => ( { id } ) ) ,
627+ }
628+ ) ;
572629
573630 if ( ! freeBusyData ) throw new Error ( "No response from google calendar" ) ;
574631 return freeBusyData . map ( ( freeBusy ) => ( { start : freeBusy . start , end : freeBusy . end } ) ) ;
@@ -590,11 +647,13 @@ export default class GoogleCalendarService implements Calendar {
590647 currentEndTime = originalEndTime ;
591648 }
592649
593- const chunkData = await this . getFreeBusyData ( {
594- timeMin : new Date ( currentStartTime ) . toISOString ( ) ,
595- timeMax : new Date ( currentEndTime ) . toISOString ( ) ,
596- items : calendarIds . map ( ( id ) => ( { id } ) ) ,
597- } ) ;
650+ const chunkData = await this . getFreeBusyData (
651+ {
652+ timeMin : new Date ( currentStartTime ) . toISOString ( ) ,
653+ timeMax : new Date ( currentEndTime ) . toISOString ( ) ,
654+ items : calendarIds . map ( ( id ) => ( { id } ) ) ,
655+ }
656+ ) ;
598657
599658 if ( chunkData ) {
600659 busyData . push ( ...chunkData . map ( ( freeBusy ) => ( { start : freeBusy . start , end : freeBusy . end } ) ) ) ;
0 commit comments