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' ;
66import { Logger } from '@rocket.chat/logger' ;
77import type { InsertionModel } from '@rocket.chat/model-typings' ;
8- import { CalendarEvent , Users } from '@rocket.chat/models' ;
8+ import { CalendarEvent } from '@rocket.chat/models' ;
99import type { UpdateResult , DeleteResult } from 'mongodb' ;
1010
11- import { applyStatusChange } from './statusEvents/applyStatusChange' ;
1211import { cancelUpcomingStatusChanges } from './statusEvents/cancelUpcomingStatusChanges' ;
1312import { removeCronJobs } from './statusEvents/removeCronJobs' ;
1413import { getShiftedTime } from './utils/getShiftedTime' ;
@@ -150,6 +149,13 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
150149 const event = await this . get ( eventId ) ;
151150 if ( event ) {
152151 await removeCronJobs ( eventId , event . uid ) ;
152+
153+ // If the event is currently in progress, clear presence immediately
154+ // otherwise the user stays "Busy" until the engine's expiration cron runs
155+ const now = new Date ( ) ;
156+ if ( event . busy !== false && event . startTime <= now && ( ! event . endTime || event . endTime > now ) ) {
157+ await Presence . endActiveState ( event . uid ) ;
158+ }
153159 }
154160
155161 const result = await CalendarEvent . deleteOne ( {
@@ -210,52 +216,29 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
210216 // 2. When a busy event is updated (time/busy status changes)
211217 // 3. When a busy event is deleted
212218 // 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)
219+ // 5. From Outlook Calendar integration (ee/server/configuration/outlookCalendar.ts)
220+ // Note: event end is handled by the presence engine's expiration cron via statusExpiresAt
221+
222+ const schedulerJobId = 'calendar-status-scheduler' ;
215223
216224 const busyStatusEnabled = settings . get < boolean > ( 'Calendar_BusyStatus_Enabled' ) ;
217225 if ( ! busyStatusEnabled ) {
218- const schedulerJobId = 'calendar-status-scheduler' ;
219226 if ( await cronJobs . has ( schedulerJobId ) ) {
220227 await cronJobs . remove ( schedulerJobId ) ;
221228 }
222229 return ;
223230 }
224231
225- const schedulerJobId = 'calendar-status-scheduler' ;
226232 if ( await cronJobs . has ( schedulerJobId ) ) {
227233 await cronJobs . remove ( schedulerJobId ) ;
228234 }
229235
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 ) {
236+ const nextStartEvent = await CalendarEvent . findNextFutureEvent ( new Date ( ) ) ;
237+ if ( ! nextStartEvent ) {
235238 return ;
236239 }
237240
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
255- return ;
256- }
257-
258- await cronJobs . addAtTimestamp ( schedulerJobId , nextProcessTime , async ( ) => this . processStatusChangesAtTime ( ) ) ;
241+ await cronJobs . addAtTimestamp ( schedulerJobId , nextStartEvent . startTime , async ( ) => this . processStatusChangesAtTime ( ) ) ;
259242 }
260243
261244 private async processStatusChangesAtTime ( ) : Promise < void > {
@@ -269,14 +252,6 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
269252 await this . processEventStart ( event ) ;
270253 }
271254
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 ;
276- }
277- await this . processEventEnd ( event ) ;
278- }
279-
280255 await this . doSetupNextStatusChange ( ) ;
281256 }
282257
@@ -285,61 +260,14 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
285260 return ;
286261 }
287262
288- const user = await Users . findOneById ( event . uid , { projection : { statusDefault : 1 } } ) ;
289- if ( ! user || user . statusDefault === UserStatus . OFFLINE ) {
290- return ;
291- }
292-
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 } ) ;
300- }
301-
302- await applyStatusChange ( {
303- eventId : event . _id ,
304- uid : event . uid ,
305- startTime : event . startTime ,
306- endTime : event . endTime ,
307- status : UserStatus . BUSY ,
263+ await Presence . setActiveState ( event . uid , {
264+ statusDefault : UserStatus . BUSY ,
265+ statusText : event . subject ,
266+ statusSource : 'external' ,
267+ statusExpiresAt : event . endTime ,
308268 } ) ;
309269 }
310270
311- private async processEventEnd ( event : ICalendarEvent ) : Promise < void > {
312- if ( ! event . endTime ) {
313- return ;
314- }
315-
316- const user = await Users . findOneById ( event . uid , { projection : { statusDefault : 1 } } ) ;
317- if ( ! user ) {
318- return ;
319- }
320-
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- }
341- }
342-
343271 private async sendCurrentNotifications ( date : Date ) : Promise < void > {
344272 const events = await CalendarEvent . findEventsToNotify ( date , 1 ) . toArray ( ) ;
345273 for await ( const event of events ) {
0 commit comments