/// <summary> /// Executes the specified context. /// </summary> /// <param name="context">The context.</param> public virtual void Execute(IJobExecutionContext context) { // Check Api connection first. if (!Zoom.ZoomAuthCheck()) { context.Result = "Zoom API authentication error. Check API settings for Zoom Room plugin or try again later."; throw new Exception("Authentication failed for Zoom API. Please verify the API settings configured in the Zoom Room plugin are valid and correct."); } using (var rockContext = new RockContext()) { #region Setup Variables int jobId = context.JobDetail.Description.AsInteger(); var job = new ServiceJobService(rockContext).GetNoTracking(jobId); var JobStartDateTime = RockDateTime.Now; DateTime?lastSuccessRunDateTime = null; if (job != null && job.Guid != Rock.SystemGuid.ServiceJob.JOB_PULSE.AsGuid()) { lastSuccessRunDateTime = job.LastSuccessfulRunDateTime; } // get the last run date or yesterday var beginDateTime = lastSuccessRunDateTime ?? JobStartDateTime.AddDays(-1); var dataMap = context.JobDetail.JobDataMap; var daysOut = dataMap.GetIntegerFromString(AttributeKey.SyncDaysOut); webhookBaseUrl = Settings.GetWebhookUrl(); var importMeetings = dataMap.GetBooleanFromString(AttributeKey.ImportMeetings); verboseLogging = dataMap.GetBooleanFromString(AttributeKey.VerboseLogging); var zrOccurrencesCancel = new List <RoomOccurrence>(); reservationLocationEntityTypeId = new EntityTypeService(rockContext).GetNoTracking(com.bemaservices.RoomManagement.SystemGuid.EntityType.RESERVATION_LOCATION.AsGuid()).Id; var zoom = Zoom.Api(); var locationService = new LocationService(rockContext); var zrLocations = locationService.Queryable() .AsNoTracking() .WhereAttributeValue(rockContext, a => a.Attribute.Key == "rocks.kfs.ZoomRoom" && a.Value != null && a.Value != "") .ToList(); var zrLocationIds = zrLocations.Select(l => l.Id).ToList(); var linkedZoomRoomLocations = new Dictionary <int, string>(); foreach (var loc in zrLocations) { loc.LoadAttributes(); var zoomRoomDV = DefinedValueCache.Get(loc.GetAttributeValue("rocks.kfs.ZoomRoom").AsGuid()); linkedZoomRoomLocations.Add(loc.Id, zoomRoomDV.Value); } #endregion Setup Variables #region Mark Completed Occurrences var zrOccurrenceService = new RoomOccurrenceService(rockContext); var completedOccurrences = zrOccurrenceService.Queryable() .Where(ro => ro.IsCompleted == false && DbFunctions.AddMinutes(ro.StartTime, ro.Duration) < beginDateTime); foreach (var occ in completedOccurrences) { occ.IsCompleted = true; } rockContext.SaveChanges(); #endregion Mark Completed Occurrences #region Cleanup var reservationLocationService = new ReservationLocationService(rockContext); var reservationLocationIds = reservationLocationService.Queryable().AsNoTracking().Select(rl => rl.Id); // Delete any orphaned RoomOccurrences ( tied to invalid/deleted ReservationId ) zrOccurrenceService = new RoomOccurrenceService(rockContext); var orphanedOccs = zrOccurrenceService.Queryable() .Where(ro => ro.EntityTypeId == reservationLocationEntityTypeId && !reservationLocationIds.Any(id => id == ro.EntityId)); if (orphanedOccs.Count() > 0) { if (verboseLogging) { LogEvent(rockContext, "Zoom Room Reservation Sync", string.Format("Preparing to delete {0} orphaned RoomOccurrence(s).")); } zrOccurrenceService.DeleteRange(orphanedOccs); var errors = new List <string>(); LogEvent(null, "Zoom Room Reservation Sync", string.Format("{0} orphaned RoomOccurrence(s) deleted.", orphanedOccs.Count())); if (verboseLogging) { LogEvent(null, "Zoom Room Reservation Sync", "Deleting related Zoom Meetings."); } DeleteOccurrenceZoomMeetings(orphanedOccs, zoom); rockContext.SaveChanges(); } // Delete any active Room Occurrences tied to Zoom Meetings that no longer exist. var linkedOccurrences = zrOccurrenceService .Queryable() .AsNoTracking() .Where(ro => ro.EntityTypeId == reservationLocationEntityTypeId && ro.ZoomMeetingId > 0 && !ro.IsCompleted && ro.StartTime >= beginDateTime); var zoomMeetings = new List <Meeting>(); foreach (var zrl in linkedZoomRoomLocations) { zoomMeetings.AddRange(zoom.GetZoomMeetings(zrl.Value, MeetingListType.Upcoming)); } var zoomMeetingIds = zoomMeetings.Select(m => m.Id).ToList(); var orphanedOccurrences = linkedOccurrences.Where(ro => !zoomMeetingIds.Any(mid => mid == ro.ZoomMeetingId)); if (orphanedOccurrences.Count() > 0) { zrOccurrenceService.DeleteRange(orphanedOccurrences); rockContext.SaveChanges(); } // Attempt to create Zoom Room Meeting for any Room Occurrences that may have had previous issues. var unlinkedOccurrences = zrOccurrenceService .Queryable() .Where(ro => ro.EntityTypeId == reservationLocationEntityTypeId && (!ro.ZoomMeetingId.HasValue || ro.ZoomMeetingId <= 0) && !ro.IsCompleted && ro.StartTime >= beginDateTime && (ro.ZoomMeetingRequestStatus == ZoomMeetingRequestStatus.Failed || ro.ZoomMeetingRequestStatus == ZoomMeetingRequestStatus.ZoomRoomOffline)); foreach (var rOcc in unlinkedOccurrences) { var rLoc = reservationLocationService.Queryable("Location").FirstOrDefault(rl => rl.Id == rOcc.EntityId); rLoc.Location.LoadAttributes(); rOcc.ZoomMeetingRequestStatus = ZoomMeetingRequestStatus.Requested; var zoomRoomDV = DefinedValueCache.Get(rLoc.Location.GetAttributeValue("rocks.kfs.ZoomRoom").AsGuid()); CreateOccurrenceZoomMeeting(rOcc, zoomRoomDV, zoom); } if (unlinkedOccurrences.Count() > 0) { rockContext.SaveChanges(); } #endregion Cleanup #region External Zoom Meetings var scheduleService = new ScheduleService(rockContext); var reservationService = new ReservationService(rockContext); var reservationTypeService = new ReservationTypeService(rockContext); var zoomImportReservationType = reservationTypeService.Get(RoomReservationType.ZOOMROOMIMPORT.AsGuid()); // Create RoomOccurrences for any Zoom Room meetings created outside of Rock if (importMeetings && linkedZoomRoomLocations.Count > 0) { var linkedMeetings = linkedOccurrences.Select(ro => ro.ZoomMeetingId).ToList(); var zoomRoomMeetings = zoomMeetings.Where(m => m.Start_Time > beginDateTime); var missingMeetings = zoomRoomMeetings.Where(m => !linkedMeetings.Any(mid => mid == m.Id)); if (missingMeetings.Count() > 0) { foreach (var zrl in linkedZoomRoomLocations) { foreach (var meeting in missingMeetings.Where(m => m.Host_Id == zrl.Value)) { // Build the iCal string as it is a required property on the Schedule for Room Reservation block to display the Reservation var meetingLocalTime = meeting.Start_Time.UtcDateTime.ToLocalTime(); var calendarEvent = new Event { DtStart = new CalDateTime(meetingLocalTime), DtEnd = new CalDateTime(meetingLocalTime.AddMinutes(meeting.Duration)), DtStamp = new CalDateTime(meetingLocalTime.Year, meetingLocalTime.Month, meetingLocalTime.Day) }; var calendar = new Calendar(); calendar.Events.Add(calendarEvent); var serializer = new CalendarSerializer(calendar); var schedule = new Schedule { Guid = Guid.NewGuid(), EffectiveStartDate = meetingLocalTime, EffectiveEndDate = meetingLocalTime.AddMinutes(meeting.Duration), IsActive = true, iCalendarContent = serializer.SerializeToString() }; scheduleService.Add(schedule); var location = locationService.Get(zrl.Key); var newReservation = new Reservation { Name = location.Name.Left(50), // NOTE: Reservation.Name is limited to 50 chars but Location.Name is up to 100 chars. ReservationTypeId = zoomImportReservationType.Id, Guid = Guid.NewGuid(), Schedule = schedule, NumberAttending = 0, Note = string.Format("Created from import of \"{0}\" meeting ({1}) from Zoom Room \"{2}\".", meeting.Topic, meeting.Id, zrl.Value), ApprovalState = ReservationApprovalState.Approved }; reservationService.Add(newReservation); var reservationLocation = new ReservationLocation { Reservation = newReservation, Location = location, ApprovalState = ReservationLocationApprovalState.Approved }; reservationLocationService.Add(reservationLocation); rockContext.SaveChanges(); var occurrence = new RoomOccurrence { ZoomMeetingId = meeting.Id, EntityTypeId = reservationLocationEntityTypeId, EntityId = reservationLocation.Id, Schedule = schedule, LocationId = reservationLocation.LocationId, Topic = meeting.Topic, StartTime = meetingLocalTime, Password = meeting.Password, Duration = meeting.Duration, TimeZone = meeting.Timezone }; zrOccurrenceService.Add(occurrence); rockContext.SaveChanges(); } } } } #endregion External Zoom Meetings #region Process Reservations var reservations = reservationService.Queryable("Schedule,ReservationLocations,ReservationType") .AsNoTracking() .Where(r => r.ModifiedDateTime >= beginDateTime && r.ReservationTypeId != zoomImportReservationType.Id && (r.ApprovalState == ReservationApprovalState.Approved || (!r.ReservationType.IsReservationBookedOnApproval && r.ApprovalState != ReservationApprovalState.Cancelled && r.ApprovalState != ReservationApprovalState.Denied && r.ApprovalState != ReservationApprovalState.Draft)) && r.ReservationLocations.Any(rl => zrLocationIds.Contains(rl.LocationId)) && r.Schedule != null && ((r.Schedule.EffectiveEndDate != null && r.Schedule.EffectiveEndDate > DbFunctions.AddDays(RockDateTime.Today, -1)) || (r.Schedule.EffectiveEndDate == null && r.Schedule.EffectiveStartDate != null && r.Schedule.EffectiveStartDate > DbFunctions.AddDays(RockDateTime.Today, -1)))) .ToList(); var resLocationIdsToProcess = new List <int>(); var zrOccurrencesAdded = 0; if (verboseLogging) { LogEvent(rockContext, "Zoom Room Reservation Sync", string.Format("{0} Room Reservation(s) to be processed", reservations.Count())); } foreach (var res in reservations) { foreach (var rl in res.ReservationLocations.Where(rl => zrLocationIds.Contains(rl.LocationId)).ToList()) { rl.Location.LoadAttributes(); var zoomRoomDV = DefinedValueCache.Get(rl.Location.AttributeValues.FirstOrDefault(v => v.Key == "rocks.kfs.ZoomRoom").Value.Value.AsGuid()); var zrPassword = zoomRoomDV.GetAttributeValue("rocks.kfs.ZoomMeetingPassword"); resLocationIdsToProcess.Add(rl.Id); var resLocOccurrences = zrOccurrenceService.Queryable().Where(ro => ro.EntityTypeId == reservationLocationEntityTypeId && ro.EntityId == rl.Id); // One-Time Schedule if (res.Schedule.EffectiveEndDate is null || res.Schedule.EffectiveStartDate.Value == res.Schedule.EffectiveEndDate.Value) { var occurrence = new RoomOccurrence(); if (resLocOccurrences.Count() == 0) { occurrence = new RoomOccurrence { Id = 0, EntityTypeId = reservationLocationEntityTypeId, EntityId = rl.Id, ScheduleId = res.ScheduleId, LocationId = rl.LocationId, Topic = res.Name, StartTime = res.Schedule.FirstStartDateTime.Value, Password = zrPassword, Duration = res.Schedule.DurationInMinutes, IsCompleted = false, ZoomMeetingRequestStatus = ZoomMeetingRequestStatus.Requested }; zrOccurrenceService.Add(occurrence); rockContext.SaveChanges(); zrOccurrencesAdded++; if (CreateOccurrenceZoomMeeting(occurrence, zoomRoomDV, zoom)) { rockContext.SaveChanges(); } } else { Meeting connectedMeeting = null; var updateMeeting = false; occurrence = resLocOccurrences.FirstOrDefault(); if (occurrence.ZoomMeetingId.HasValue && occurrence.ZoomMeetingId.Value > 0) { connectedMeeting = zoomMeetings.FirstOrDefault(m => m.Id == occurrence.ZoomMeetingId.Value); if (connectedMeeting == null) { occurrence.ZoomMeetingId = null; } } if (occurrence.IsCompleted) { occurrence.IsCompleted = false; } if (occurrence.ScheduleId != res.ScheduleId) { occurrence.ScheduleId = res.ScheduleId; } if (occurrence.StartTime != res.Schedule.FirstStartDateTime.Value) { occurrence.StartTime = res.Schedule.FirstStartDateTime.Value; } if (connectedMeeting != null && connectedMeeting.Start_Time != occurrence.StartTime.ToRockDateTimeOffset()) { connectedMeeting.Start_Time = occurrence.StartTime.ToRockDateTimeOffset(); updateMeeting = true; } if (occurrence.Duration != res.Schedule.DurationInMinutes) { occurrence.Duration = res.Schedule.DurationInMinutes; if (connectedMeeting != null) { connectedMeeting.Duration = res.Schedule.DurationInMinutes; updateMeeting = true; } } if (occurrence.Topic != res.Name) { occurrence.Topic = res.Name; if (connectedMeeting != null) { connectedMeeting.Topic = res.Name; updateMeeting = true; } } if (updateMeeting) { zoom.UpdateMeeting(connectedMeeting); } } }