@@ -8,10 +8,11 @@ import type {
88} from "@prisma/client" ;
99import * as Sentry from "@sentry/nextjs" ;
1010import { z } from "zod" ;
11- import type { BookingRepository } from "@calcom/lib/server/repository/booking" ;
1211
12+ import { getCalendar } from "@calcom/app-store/_utils/getCalendar" ;
1313import type { Dayjs } from "@calcom/dayjs" ;
1414import dayjs from "@calcom/dayjs" ;
15+ import type { IRedisService } from "@calcom/features/redis/IRedisService" ;
1516import { getWorkingHours } from "@calcom/lib/availability" ;
1617import type { DateOverride , WorkingHours } from "@calcom/lib/date-ranges" ;
1718import { buildDateRanges , subtract } from "@calcom/lib/date-ranges" ;
@@ -27,15 +28,16 @@ import {
2728import logger from "@calcom/lib/logger" ;
2829import { safeStringify } from "@calcom/lib/safeStringify" ;
2930import { findUsersForAvailabilityCheck } from "@calcom/lib/server/findUsersForAvailabilityCheck" ;
31+ import type { BookingRepository } from "@calcom/lib/server/repository/booking" ;
3032import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository" ;
33+ import type { PrismaOOORepository } from "@calcom/lib/server/repository/ooo" ;
3134import { SchedulingType } from "@calcom/prisma/enums" ;
3235import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils" ;
3336import type { EventBusyDetails , IntervalLimitUnit } from "@calcom/types/Calendar" ;
3437import type { TimeRange } from "@calcom/types/schedule" ;
3538
3639import { getBusyTimes } from "./getBusyTimes" ;
3740import { withReporting } from "./sentryWrapper" ;
38- import type { PrismaOOORepository } from "@calcom/lib/server/repository/ooo" ;
3941
4042const log = logger . getSubLogger ( { prefix : [ "getUserAvailability" ] } ) ;
4143const availabilitySchema = z
@@ -55,9 +57,7 @@ const availabilitySchema = z
5557 } )
5658 . refine ( ( data ) => ! ! data . username || ! ! data . userId , "Either username or userId should be filled in." ) ;
5759
58- export type EventType = Awaited <
59- ReturnType < ( typeof UserAvailabilityService ) [ "prototype" ] [ "_getEventType" ] >
60- > ;
60+ export type EventType = Awaited < ReturnType < ( typeof UserAvailabilityService ) [ "prototype" ] [ "_getEventType" ] > > ;
6161
6262type GetUser = Awaited < ReturnType < ( typeof UserAvailabilityService ) [ "prototype" ] [ "_getUser" ] > > ;
6363
@@ -157,13 +157,71 @@ export interface IUserAvailabilityService {
157157 eventTypeRepo : EventTypeRepository ;
158158 oooRepo : PrismaOOORepository ;
159159 bookingRepo : BookingRepository ;
160+ redisClient : IRedisService ;
160161}
161162
162163export class UserAvailabilityService {
163164 constructor ( public readonly dependencies : IUserAvailabilityService ) { }
164165
166+ // Fetch timezones from outlook or google using delegated credentials (formely known as domain wide delegatiion)
167+ async getTimezoneFromDelegatedCalendars ( user : GetAvailabilityUser ) : Promise < string | null > {
168+ if ( ! user . credentials || user . credentials . length === 0 ) {
169+ return null ;
170+ }
171+
172+ const delegatedCredentials = user . credentials . filter (
173+ ( credential ) => credential . type . endsWith ( "_calendar" ) && Boolean ( credential . delegatedToId )
174+ ) ;
175+
176+ if ( ! delegatedCredentials || delegatedCredentials . length === 0 ) {
177+ return null ;
178+ }
179+
180+ const cacheKey = `user-timezone:${ user . id } ` ;
181+
182+ try {
183+ const cachedTimezone = await this . dependencies . redisClient . get < string > ( cacheKey ) ;
184+
185+ if ( cachedTimezone ) {
186+ log . debug ( `Got timezone ${ cachedTimezone } from Redis cache for user ${ user . id } ` ) ;
187+ return cachedTimezone ;
188+ }
189+ } catch ( error ) {
190+ log . warn ( `Failed to get timezone from Redis cache for user ${ user . id } :` , error ) ;
191+ }
192+
193+ if ( delegatedCredentials . length === 0 ) {
194+ return null ;
195+ }
196+
197+ for ( const credential of delegatedCredentials ) {
198+ try {
199+ const calendar = await getCalendar ( credential ) ;
200+ if ( calendar && "getMainTimeZone" in calendar && typeof calendar . getMainTimeZone === "function" ) {
201+ const timezone = await calendar . getMainTimeZone ( ) ;
202+ if ( timezone && timezone !== "UTC" ) {
203+ log . debug ( `Got timezone ${ timezone } from calendar service ${ credential . type } ` ) ;
204+
205+ try {
206+ await this . dependencies . redisClient . set < string > ( cacheKey , timezone , { ttl : 3600 * 6 * 1000 } ) ; // 6 hours ttl in ms;
207+ log . debug ( `Cached timezone ${ timezone } in Redis for user ${ user . id } ` ) ;
208+ } catch ( error ) {
209+ log . warn ( `Failed to set timezone in Redis cache for user ${ user . id } :` , error ) ;
210+ }
211+
212+ return timezone ;
213+ }
214+ }
215+ } catch ( error ) {
216+ log . warn ( `Failed to get timezone from calendar service ${ credential . type } :` , error ) ;
217+ }
218+ }
219+
220+ return null ;
221+ }
222+
165223 async _getEventType ( id : number ) {
166- const eventType = await this . dependencies . eventTypeRepo . findByIdForUserAvailability ( { id } )
224+ const eventType = await this . dependencies . eventTypeRepo . findByIdForUserAvailability ( { id } ) ;
167225 if ( ! eventType ) {
168226 return eventType ;
169227 }
@@ -200,9 +258,11 @@ export class UserAvailabilityService {
200258 schedulingType === SchedulingType . ROUND_ROBIN ||
201259 schedulingType === SchedulingType . COLLECTIVE ;
202260
203- const bookings = await this . dependencies . bookingRepo . findAcceptedBookingByEventTypeId ( { eventTypeId : id , dateFrom : dateFrom . format ( ) , dateTo : dateTo . format ( ) } )
204-
205-
261+ const bookings = await this . dependencies . bookingRepo . findAcceptedBookingByEventTypeId ( {
262+ eventTypeId : id ,
263+ dateFrom : dateFrom . format ( ) ,
264+ dateTo : dateTo . format ( ) ,
265+ } ) ;
206266
207267 return bookings . map ( ( booking ) => {
208268 const attendees = isTeamEvent
@@ -290,10 +350,15 @@ export class UserAvailabilityService {
290350 timeZone : fallbackTimezoneIfScheduleIsMissing ,
291351 } ;
292352
293- const schedule =
294- ( eventType ?. schedule ? eventType . schedule : hostSchedule ? hostSchedule : userSchedule ) ??
295- fallbackSchedule ;
296- const timeZone = schedule ?. timeZone || fallbackTimezoneIfScheduleIsMissing ;
353+ // possible timezones that have been set by or for a user
354+ const potentialSchedule = eventType ?. schedule
355+ ? eventType . schedule
356+ : hostSchedule
357+ ? hostSchedule
358+ : userSchedule ;
359+
360+ // if no schedules set by or for a user, use fallbackSchedule
361+ const schedule = potentialSchedule ?? fallbackSchedule ;
297362
298363 const bookingLimits =
299364 eventType ?. bookingLimits &&
@@ -309,6 +374,25 @@ export class UserAvailabilityService {
309374 ? parseDurationLimit ( eventType . durationLimits )
310375 : null ;
311376
377+ // TODO: only query what we need after applying limits (shrink date range)
378+ const getBusyTimesStart = dateFrom . toISOString ( ) ;
379+ const getBusyTimesEnd = dateTo . toISOString ( ) ;
380+
381+ const selectedCalendars = eventType ?. useEventLevelSelectedCalendars
382+ ? EventTypeRepository . getSelectedCalendarsFromUser ( { user, eventTypeId : eventType . id } )
383+ : user . userLevelSelectedCalendars ;
384+
385+ const isTimezoneSet = Boolean ( potentialSchedule && potentialSchedule . timeZone !== null ) ;
386+
387+ // this timezone is synced with google/outlook calendars timezone usingg delegated credentials
388+ // it's a fallback for delegated credentials users who want to sync their timezone with third party calendars
389+ const calendarTimezone = ! isTimezoneSet ? await this . getTimezoneFromDelegatedCalendars ( user ) : null ;
390+
391+ const finalTimezone =
392+ ! isTimezoneSet && calendarTimezone
393+ ? calendarTimezone
394+ : schedule ?. timeZone || fallbackTimezoneIfScheduleIsMissing ;
395+
312396 let busyTimesFromLimits : EventBusyDetails [ ] = [ ] ;
313397
314398 if ( initialData ?. busyTimesFromLimits && initialData ?. eventTypeForLimits ) {
@@ -318,12 +402,12 @@ export class UserAvailabilityService {
318402 busyTimesFromLimits = await getBusyTimesFromLimits (
319403 bookingLimits ,
320404 durationLimits ,
321- dateFrom . tz ( timeZone ) ,
322- dateTo . tz ( timeZone ) ,
405+ dateFrom . tz ( finalTimezone ) ,
406+ dateTo . tz ( finalTimezone ) ,
323407 duration ,
324408 eventType ,
325409 initialData ?. busyTimesFromLimitsBookings ?? [ ] ,
326- timeZone ,
410+ finalTimezone ,
327411 initialData ?. rescheduleUid ?? undefined
328412 ) ;
329413 }
@@ -344,23 +428,15 @@ export class UserAvailabilityService {
344428 busyTimesFromTeamLimits = await getBusyTimesFromTeamLimits (
345429 user ,
346430 teamBookingLimits ,
347- dateFrom . tz ( timeZone ) ,
348- dateTo . tz ( timeZone ) ,
431+ dateFrom . tz ( finalTimezone ) ,
432+ dateTo . tz ( finalTimezone ) ,
349433 teamForBookingLimits . id ,
350434 teamForBookingLimits . includeManagedEventsInLimits ,
351- timeZone ,
435+ finalTimezone ,
352436 initialData ?. rescheduleUid ?? undefined
353437 ) ;
354438 }
355439
356- // TODO: only query what we need after applying limits (shrink date range)
357- const getBusyTimesStart = dateFrom . toISOString ( ) ;
358- const getBusyTimesEnd = dateTo . toISOString ( ) ;
359-
360- const selectedCalendars = eventType ?. useEventLevelSelectedCalendars
361- ? EventTypeRepository . getSelectedCalendarsFromUser ( { user, eventTypeId : eventType . id } )
362- : user . userLevelSelectedCalendars ;
363-
364440 let busyTimes = [ ] ;
365441 try {
366442 busyTimes = await getBusyTimes ( {
@@ -385,7 +461,7 @@ export class UserAvailabilityService {
385461 log . error ( `Error fetching busy times for user ${ username } :` , error ) ;
386462 return {
387463 busy : [ ] ,
388- timeZone,
464+ timeZone : finalTimezone ,
389465 dateRanges : [ ] ,
390466 oooExcludedDateRanges : [ ] ,
391467 workingHours : [ ] ,
@@ -434,7 +510,7 @@ export class UserAvailabilityService {
434510 userId : user . id ,
435511 } ) ) ;
436512
437- const workingHours = getWorkingHours ( { timeZone } , availability ) ;
513+ const workingHours = getWorkingHours ( { timeZone : finalTimezone } , availability ) ;
438514
439515 const dateOverrides : TimeRange [ ] = [ ] ;
440516 // NOTE: getSchedule is currently calling this function for every user in a team event
@@ -466,15 +542,19 @@ export class UserAvailabilityService {
466542
467543 const outOfOfficeDays =
468544 initialData ?. outOfOfficeDays ??
469- ( await this . dependencies . oooRepo . findUserOOODays ( { userId : user . id , dateFrom : dateFrom . toISOString ( ) , dateTo : dateTo . toISOString ( ) } ) ) ;
545+ ( await this . dependencies . oooRepo . findUserOOODays ( {
546+ userId : user . id ,
547+ dateFrom : dateFrom . toISOString ( ) ,
548+ dateTo : dateTo . toISOString ( ) ,
549+ } ) ) ;
470550
471551 const datesOutOfOffice : IOutOfOfficeData = this . calculateOutOfOfficeRanges ( outOfOfficeDays , availability ) ;
472552
473553 const { dateRanges, oooExcludedDateRanges } = buildDateRanges ( {
474554 dateFrom,
475555 dateTo,
476556 availability,
477- timeZone,
557+ timeZone : finalTimezone ,
478558 travelSchedules : isDefaultSchedule
479559 ? user . travelSchedules . map ( ( schedule ) => {
480560 return {
@@ -497,7 +577,7 @@ export class UserAvailabilityService {
497577
498578 const result = {
499579 busy : detailedBusyTimes ,
500- timeZone,
580+ timeZone : finalTimezone ,
501581 dateRanges : dateRangesInWhichUserIsAvailable ,
502582 oooExcludedDateRanges : dateRangesInWhichUserIsAvailableWithoutOOO ,
503583 workingHours,
@@ -610,4 +690,4 @@ export class UserAvailabilityService {
610690 }
611691
612692 getUsersAvailability = withReporting ( this . _getUsersAvailability . bind ( this ) , "getUsersAvailability" ) ;
613- }
693+ }
0 commit comments