11import type { ICalendarService } from '@rocket.chat/core-services' ;
2- import { ServiceClassInternal , api } from '@rocket.chat/core-services' ;
2+ import { Presence , ServiceClassInternal , api } from '@rocket.chat/core-services' ;
33import type { IUser , ICalendarEvent } from '@rocket.chat/core-typings' ;
44import { UserStatus } from '@rocket.chat/core-typings' ;
55import { cronJobs } from '@rocket.chat/cron' ;
@@ -8,12 +8,10 @@ import type { InsertionModel } from '@rocket.chat/model-typings';
88import { CalendarEvent , Users } from '@rocket.chat/models' ;
99import type { UpdateResult , DeleteResult } from 'mongodb' ;
1010
11- import { applyStatusChange } from './statusEvents/applyStatusChange' ;
12- import { cancelUpcomingStatusChanges } from './statusEvents/cancelUpcomingStatusChanges' ;
13- import { removeCronJobs } from './statusEvents/removeCronJobs' ;
1411import { getShiftedTime } from './utils/getShiftedTime' ;
1512import { settings } from '../../../app/settings/server' ;
1613import { getUserPreference } from '../../../app/utils/server/lib/getUserPreference' ;
14+ import { i18n } from '../../lib/i18n' ;
1715
1816const logger = new Logger ( 'Calendar' ) ;
1917
@@ -44,6 +42,7 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
4442 await this . setupNextNotification ( ) ;
4543 if ( busy !== false ) {
4644 await this . setupNextStatusChange ( ) ;
45+ await this . reconcileInProgressEvent ( insertResult . insertedId ) ;
4746 }
4847
4948 return insertResult . insertedId ;
@@ -83,6 +82,7 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
8382 await this . setupNextNotification ( ) ;
8483 if ( busy !== false ) {
8584 await this . setupNextStatusChange ( ) ;
85+ await this . reconcileInProgressEvent ( insertResult . insertedId ) ;
8686 }
8787
8888 return insertResult . insertedId ;
@@ -93,6 +93,7 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
9393 await this . setupNextNotification ( ) ;
9494 if ( busy !== false ) {
9595 await this . setupNextStatusChange ( ) ;
96+ await this . reconcileInProgressEvent ( event . _id ) ;
9697 }
9798 }
9899
@@ -135,10 +136,10 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
135136 await this . setupNextNotification ( ) ;
136137
137138 if ( startTime || endTime ) {
138- await removeCronJobs ( eventId , event . uid ) ;
139139 const isBusy = busy !== undefined ? busy : event . busy !== false ;
140140 if ( isBusy ) {
141141 await this . setupNextStatusChange ( ) ;
142+ await this . reconcileInProgressEvent ( eventId ) ;
142143 }
143144 }
144145 }
@@ -148,16 +149,20 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
148149
149150 public async delete ( eventId : ICalendarEvent [ '_id' ] ) : Promise < DeleteResult > {
150151 const event = await this . get ( eventId ) ;
151- if ( event ) {
152- await removeCronJobs ( eventId , event . uid ) ;
153- }
152+ const now = new Date ( ) ;
153+ const wasInProgress = Boolean ( event && event . busy !== false && event . startTime <= now && ( ! event . endTime || event . endTime > now ) ) ;
154154
155155 const result = await CalendarEvent . deleteOne ( {
156156 _id : eventId ,
157157 } ) ;
158158
159159 if ( result . deletedCount > 0 ) {
160160 await this . setupNextStatusChange ( ) ;
161+
162+ // deleting an in-progress busy event: recompute the claim from the events still active
163+ if ( event && wasInProgress ) {
164+ await this . syncBusyPresence ( event . uid , { excludeEventId : eventId , now } ) ;
165+ }
161166 }
162167
163168 return result ;
@@ -171,10 +176,6 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
171176 return this . doSetupNextStatusChange ( ) ;
172177 }
173178
174- public async cancelUpcomingStatusChanges ( uid : IUser [ '_id' ] , endTime = new Date ( ) ) : Promise < void > {
175- return cancelUpcomingStatusChanges ( uid , endTime ) ;
176- }
177-
178179 private async getMeetingUrl ( eventData : Partial < ICalendarEvent > ) : Promise < string | undefined > {
179180 if ( eventData . meetingUrl !== undefined ) {
180181 return eventData . meetingUrl || undefined ;
@@ -205,139 +206,104 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
205206 }
206207
207208 private async doSetupNextStatusChange ( ) : Promise < void > {
208- // This method is called in the following moments:
209- // 1. When a new busy event is created or imported
210- // 2. When a busy event is updated (time/busy status changes)
211- // 3. When a busy event is deleted
212- // 4. When a status change job executes and completes
213- // 5. When an event ends and the status is restored
214- // 6. From Outlook Calendar integration (ee/server/configuration/outlookCalendar.ts)
209+ // Schedules only event starts; event end is handled by the presence engine's expiration cron.
210+
211+ const schedulerJobId = 'calendar-status-scheduler' ;
215212
216213 const busyStatusEnabled = settings . get < boolean > ( 'Calendar_BusyStatus_Enabled' ) ;
217214 if ( ! busyStatusEnabled ) {
218- const schedulerJobId = 'calendar-status-scheduler' ;
219215 if ( await cronJobs . has ( schedulerJobId ) ) {
220216 await cronJobs . remove ( schedulerJobId ) ;
221217 }
222218 return ;
223219 }
224220
225- const schedulerJobId = 'calendar-status-scheduler' ;
226221 if ( await cronJobs . has ( schedulerJobId ) ) {
227222 await cronJobs . remove ( schedulerJobId ) ;
228223 }
229224
230- const now = new Date ( ) ;
231- const nextStartEvent = await CalendarEvent . findNextFutureEvent ( now ) ;
232- const inProgressEvents = await CalendarEvent . findInProgressEvents ( now ) . toArray ( ) ;
233- const eventsWithEndTime = inProgressEvents . filter ( ( event ) => event . endTime && event . busy !== false ) ;
234- if ( eventsWithEndTime . length === 0 && ! nextStartEvent ) {
235- return ;
236- }
237-
238- let nextEndTime : Date | null = null ;
239- if ( eventsWithEndTime . length > 0 && eventsWithEndTime [ 0 ] . endTime ) {
240- nextEndTime = eventsWithEndTime . reduce ( ( earliest , event ) => {
241- if ( ! event . endTime ) return earliest ;
242- return event . endTime . getTime ( ) < earliest . getTime ( ) ? event . endTime : earliest ;
243- } , eventsWithEndTime [ 0 ] . endTime ) ;
244- }
245-
246- let nextProcessTime : Date ;
247- if ( nextStartEvent && nextEndTime ) {
248- nextProcessTime = nextStartEvent . startTime . getTime ( ) < nextEndTime . getTime ( ) ? nextStartEvent . startTime : nextEndTime ;
249- } else if ( nextStartEvent ) {
250- nextProcessTime = nextStartEvent . startTime ;
251- } else if ( nextEndTime ) {
252- nextProcessTime = nextEndTime ;
253- } else {
254- // This should never happen due to the earlier check, but just in case
225+ const nextStartEvent = await CalendarEvent . findNextFutureEvent ( new Date ( ) ) ;
226+ if ( ! nextStartEvent ) {
255227 return ;
256228 }
257229
258- await cronJobs . addAtTimestamp ( schedulerJobId , nextProcessTime , async ( ) => this . processStatusChangesAtTime ( ) ) ;
230+ await cronJobs . addAtTimestamp ( schedulerJobId , nextStartEvent . startTime , async ( ) => this . processStatusChangesAtTime ( ) ) ;
259231 }
260232
261233 private async processStatusChangesAtTime ( ) : Promise < void > {
262234 const processTime = new Date ( ) ;
263235
264- const eventsStartingNow = await CalendarEvent . findEventsStartingNow ( { now : processTime , offset : 5000 } ) . toArray ( ) ;
265- for await ( const event of eventsStartingNow ) {
266- if ( event . busy === false ) {
267- continue ;
268- }
269- await this . processEventStart ( event ) ;
270- }
271-
272- const eventsEndingNow = await CalendarEvent . findEventsEndingNow ( { now : processTime , offset : 5000 } ) . toArray ( ) ;
273- for await ( const event of eventsEndingNow ) {
274- if ( event . busy === false ) {
275- continue ;
236+ try {
237+ const eventsStartingNow = await CalendarEvent . findEventsStartingNow ( { now : processTime , offset : 5000 } ) . toArray ( ) ;
238+ for await ( const event of eventsStartingNow ) {
239+ if ( event . busy === false ) {
240+ continue ;
241+ }
242+ await this . processEventStart ( event ) ;
276243 }
277- await this . processEventEnd ( event ) ;
244+ } catch ( err ) {
245+ logger . error ( { msg : 'Failed to process calendar status changes' , err } ) ;
278246 }
279247
280248 await this . doSetupNextStatusChange ( ) ;
281249 }
282250
283251 private async processEventStart ( event : ICalendarEvent ) : Promise < void > {
252+ // no endTime → no expiry to set, so the claim could never auto-clear
284253 if ( ! event . endTime ) {
285254 return ;
286255 }
287256
288- const user = await Users . findOneById ( event . uid , { projection : { statusDefault : 1 } } ) ;
289- if ( ! user || user . statusDefault === UserStatus . OFFLINE ) {
257+ await this . syncBusyPresence ( event . uid , { excludeEventId : event . _id , seedEndTime : event . endTime } ) ;
258+ }
259+
260+ // The start scheduler only fires at start times, so it misses an event imported already in progress.
261+ private async reconcileInProgressEvent ( eventId : ICalendarEvent [ '_id' ] ) : Promise < void > {
262+ const event = await CalendarEvent . findOne ( { _id : eventId } ) ;
263+ if ( ! event ?. endTime || event . busy === false ) {
290264 return ;
291265 }
292266
293- const overlappingEvents = await CalendarEvent . findOverlappingEvents ( event . _id , event . uid , event . startTime , event . endTime )
294- . sort ( { startTime : - 1 } )
295- . toArray ( ) ;
296- const previousStatus = overlappingEvents . at ( 0 ) ?. previousStatus ?? user . statusDefault ;
297-
298- if ( previousStatus ) {
299- await CalendarEvent . updateEvent ( event . _id , { previousStatus } ) ;
267+ const now = new Date ( ) ;
268+ if ( event . startTime > now || event . endTime <= now ) {
269+ return ;
300270 }
301271
302- await applyStatusChange ( {
303- eventId : event . _id ,
304- uid : event . uid ,
305- startTime : event . startTime ,
306- endTime : event . endTime ,
307- status : UserStatus . BUSY ,
308- } ) ;
272+ await this . syncBusyPresence ( event . uid , { excludeEventId : event . _id , seedEndTime : event . endTime , now } ) ;
309273 }
310274
311- private async processEventEnd ( event : ICalendarEvent ) : Promise < void > {
312- if ( ! event . endTime ) {
275+ // Derives "busy until the latest active meeting ends" from the events in progress now and applies
276+ // it as one calendar claim. `excludeEventId`/`seedEndTime` re-add the triggering event's own end.
277+ private async syncBusyPresence (
278+ uid : IUser [ '_id' ] ,
279+ { excludeEventId, seedEndTime, now = new Date ( ) } : { excludeEventId ?: ICalendarEvent [ '_id' ] ; seedEndTime ?: Date ; now ?: Date } = { } ,
280+ ) : Promise < void > {
281+ if ( ! settings . get < boolean > ( 'Calendar_BusyStatus_Enabled' ) ) {
313282 return ;
314283 }
315284
316- const user = await Users . findOneById ( event . uid , { projection : { statusDefault : 1 } } ) ;
317- if ( ! user ) {
285+ const overlappingEvents = await CalendarEvent . findOverlappingEvents ( excludeEventId ?? '' , uid , now , now ) . toArray ( ) ;
286+ const endTimes = [ ...( seedEndTime ? [ seedEndTime ] : [ ] ) , ...overlappingEvents . map ( ( event ) => event . endTime ) ] . filter (
287+ ( date ) : date is Date => Boolean ( date ) ,
288+ ) ;
289+
290+ // No busy event left → end the calendar claim (no-op if a higher-priority status took over).
291+ if ( endTimes . length === 0 ) {
292+ await Presence . endActiveState ( uid , this . name ) ;
318293 return ;
319294 }
320295
321- // Only restore status if:
322- // 1. The current statusDefault is BUSY (meaning it was set by our system, not manually changed by user)
323- // 2. We have a previousStatus stored from before the event started
324-
325- if ( user . statusDefault === UserStatus . BUSY && event . previousStatus && event . previousStatus !== user . statusDefault ) {
326- await applyStatusChange ( {
327- eventId : event . _id ,
328- uid : event . uid ,
329- startTime : event . startTime ,
330- endTime : event . endTime ,
331- status : event . previousStatus ,
332- } ) ;
333- } else {
334- logger . debug ( {
335- msg : 'Not restoring status for user' ,
336- userId : event . uid ,
337- currentStatusDefault : user . statusDefault ,
338- previousStatus : event . previousStatus ,
339- } ) ;
340- }
296+ const statusExpiresAt = endTimes . reduce ( ( latest , date ) => ( date > latest ? date : latest ) ) ;
297+ const user = await Users . findOneById < Pick < IUser , '_id' | 'language' > > ( uid , { projection : { language : 1 } } ) ;
298+ const lng = user ?. language || settings . get < string > ( 'Language' ) || 'en' ;
299+
300+ await Presence . setActiveState ( uid , {
301+ statusDefault : UserStatus . BUSY ,
302+ statusText : i18n . t ( 'Presence_status_in_a_meeting' , { lng } ) ,
303+ statusSource : 'external' ,
304+ statusExpiresAt,
305+ statusId : this . name ,
306+ } ) ;
341307 }
342308
343309 private async sendCurrentNotifications ( date : Date ) : Promise < void > {
0 commit comments