/// <summary> /// Loads the reservations to be displayed, and optionally performs a checkin or checkout /// if the corresponding reservation reference id is provided. The checkin/checkout comes /// first so we retrieve the reservation list AFTER those changes are applied. /// </summary> /// <param name="checkin">Optional reference id of a reservation to check in</param> /// <param name="checkout">Optional reference id of a reservation to check out</param> /// <returns>Task returning an error message (or null) from the checkin or checkout</returns> public async Task<string> LoadDataAsync(string checkin, string checkout) { var clubInfo = EnvironmentDefinition.Instance.MapClubIdToClubInfo[this.ClubId]; if (!BookedSchedulerCache.Instance[this.ClubId].IsInitialized) { this.Reservations = new JArray(); return $"Please wait while the BoatTracker service initializes. This page will automatically refresh in one minute."; } // We only need the timezone for the user. this.BotUserState = await BookedSchedulerCache.Instance[this.ClubId].GetBotUserStateAsync(); BookedSchedulerRetryClient client = null; try { client = new BookedSchedulerRetryClient(this.ClubId, true); await client.SignInAsync(clubInfo.UserName, clubInfo.Password); string message = null; if (!string.IsNullOrEmpty(checkin)) { try { await client.CheckInReservationAsync(checkin); } catch (Exception ex) { message = $"Checkin failed: {ex.Message}"; } } else if (!string.IsNullOrEmpty(checkout)) { try { await client.CheckOutReservationAsync(checkout); } catch (Exception ex) { message = $"Checkout failed: {ex.Message}"; } } // TODO: figure out how to narrow this down var reservations = await client.GetReservationsAsync( start: DateTime.UtcNow - TimeSpan.FromDays(1), end: DateTime.UtcNow + TimeSpan.FromDays(1)); this.Reservations = reservations; return message; } catch (Exception ex) { this.Reservations = new JArray(); return $"Unable to fetch reservations, currently. This page will automatically refresh in one minute. ({ex.Message})"; } finally { if (client != null && client.IsSignedIn) { try { await client.SignOutAsync(); } catch (Exception) { // best effort only } } } }
public async Task RefreshCacheAsync(bool failSilently = true) { BookedSchedulerRetryClient client = null; try { // // If there's a refresh in progress on another thread, we return and let // the caller proceed with date that's soon to be replaced. The priority // is to ensure that two threads aren't updating the cache at once. // if (Interlocked.CompareExchange(ref this.refreshInProgress, 1, 0) != 0) { return; } var clubInfo = EnvironmentDefinition.Instance.MapClubIdToClubInfo[this.clubId]; client = new BookedSchedulerRetryClient(this.clubId, false); await client.SignInAsync(clubInfo.UserName, clubInfo.Password); var newResources = await client.GetResourcesAsync(); var users = await client.GetUsersAsync(); var newUserMap = new Dictionary<long, JToken>(); foreach (var u in users) { var fullUser = await client.GetUserAsync(u.Id()); newUserMap.Add(fullUser.Id(), fullUser); } var groups = await client.GetGroupsAsync(); var newGroupMap = new Dictionary<long, JToken>(); foreach (var g in groups) { var fullGroup = await client.GetGroupAsync(g.Id()); newGroupMap.Add(fullGroup.Id(), fullGroup); } #if UNUSED var newSchedules = await client.GetSchedulesAsync(); this.schedules = newSchedules; #endif this.resources = newResources; this.userMap = newUserMap; this.groupMap = newGroupMap; // Schedule the next cache refresh this.refreshTime = DateTime.Now + CacheTimeout; } catch (Exception) { // // During normal operation, we fail silently since we have existing (but stale) data // that we can continue to use. During initialization, we must rethrow so we can retry. // if (failSilently) { // If there were any errors, leave the stale data in place and schedule another // refresh fairly soon. this.refreshTime = DateTime.Now + CacheRetryTime; } else { throw; } } finally { this.refreshInProgress = 0; if (client != null && client.IsSignedIn) { try { await client.SignOutAsync(); } catch (Exception) { // best effort only } } } }
private static async Task<ValidateResult> ValidatePassword(SignInForm state, object value) { string password = (string)value; var clubInfo = EnvironmentDefinition.Instance.MapClubIdToClubInfo[state.ClubInitials]; var client = new BookedSchedulerRetryClient(clubInfo.Id, true); try { await client.SignInAsync(state.UserName, password); } catch (HttpRequestException) { return new ValidateResult { IsValid = false, Value = null, Feedback = $"I'm sorry but your password is incorrect. Please try again." }; } finally { if (client != null && client.IsSignedIn) { try { await client.SignOutAsync(); } catch (Exception) { // best effort only } } } return new ValidateResult { IsValid = true, Value = password }; }
/// <summary> /// Generates a daily report of usage and policy violations and emails it to the configured recipients. /// </summary> /// <param name="clubId">The club of interest</param> /// <param name="log">The writer for logging.</param> /// <returns>A task that completes when the daily report has been generated and sent.</returns> private static async Task RunDailyReport(string clubId, TextWriter log) { var clubInfo = EnvironmentDefinition.Instance.MapClubIdToClubInfo[clubId]; var client = new BookedSchedulerRetryClient(clubId, false); await client.SignInAsync(clubInfo.UserName, clubInfo.Password); // Get all reservations for the last day var reservations = await client.GetReservationsAsync(start: DateTime.UtcNow - TimeSpan.FromDays(1), end: DateTime.UtcNow); var compliant = new List<string>(); var abandoned = new List<string>(); var failedToCheckOut = new List<string>(); var unknownParticipants = new List<string>(); var withGuest = new List<string>(); foreach (var reservation in reservations) { DateTime? checkInDate = reservation.CheckInDate(); DateTime? checkOutDate = reservation.CheckOutDate(); var user = await client.GetUserAsync(reservation.UserId()); var boatName = reservation.ResourceName(); var localStartTime = ConvertToLocalTime(user, reservation.StartDate()).ToShortTimeString(); var localEndTime = ConvertToLocalTime(user, reservation.EndDate()).ToShortTimeString(); string basicInfo = $"{user.FullName()} ({user.EmailAddress()}) - '{boatName}' @ {localStartTime} - {localEndTime}"; if (checkInDate.HasValue) { var localCheckInTime = ConvertToLocalTime(user, checkInDate.Value).ToShortTimeString(); if (checkOutDate.HasValue) { var localCheckOutTime = ConvertToLocalTime(user, checkOutDate.Value).ToShortTimeString(); compliant.Add($"{basicInfo} (actual: {localCheckInTime} - {localCheckOutTime})"); } else { failedToCheckOut.Add($"{basicInfo} (actual: {localCheckInTime} - ??)"); } } else { abandoned.Add(basicInfo); } var invitedGuests = reservation["invitedGuests"] as JArray ?? new JArray(); var participatingGuests = reservation["participatingGuests"] as JArray ?? new JArray(); // Get the "full" reservation to make sure the participants list is given as an array var fullReservation = await client.GetReservationAsync(reservation.ReferenceNumber()); var participants = (JArray)fullReservation["participants"]; var boat = await client.GetResourceAsync(reservation.ResourceId()); // // See if the number of recorded participants is less than the boat capacity. We always // have to add one for the reservation owner. // if (participants.Count + invitedGuests.Count + participatingGuests.Count + 1 < boat.MaxParticipants()) { unknownParticipants.Add(basicInfo); } // Check for reservations involving a guest rower if (invitedGuests.Count > 0 || participatingGuests.Count > 0) { var guestEmail = invitedGuests.Count > 0 ? invitedGuests[0].Value<string>() : participatingGuests[0].Value<string>(); withGuest.Add($"{basicInfo} with {guestEmail}"); } } // Get all reservations for the next two days var upcomingReservations = await client.GetReservationsAsync(start: DateTime.UtcNow, end: DateTime.UtcNow + TimeSpan.FromDays(2)); var upcomingWithGuest = new List<string>(); foreach (var reservation in upcomingReservations) { var invitedGuests = reservation["invitedGuests"] as JArray ?? new JArray(); var participatingGuests = reservation["participatingGuests"] as JArray ?? new JArray(); if (invitedGuests.Count > 0 || participatingGuests.Count > 0) { DateTime? checkInDate = reservation.CheckInDate(); DateTime? checkOutDate = reservation.CheckOutDate(); var user = await client.GetUserAsync(reservation.UserId()); var boatName = reservation.ResourceName(); var localStartDate = ConvertToLocalTime(user, reservation.StartDate()).ToShortDateString(); var localStartTime = ConvertToLocalTime(user, reservation.StartDate()).ToShortTimeString(); var localEndTime = ConvertToLocalTime(user, reservation.EndDate()).ToShortTimeString(); string basicInfo = $"{user.FullName()} ({user.EmailAddress()}) - '{boatName}' @ {localStartDate} {localStartTime} - {localEndTime}"; var guestEmail = invitedGuests.Count > 0 ? invitedGuests[0].Value<string>() : participatingGuests[0].Value<string>(); upcomingWithGuest.Add($"{basicInfo} with {guestEmail}"); } } await client.SignOutAsync(); var body = new StringBuilder(); body.AppendLine($"<h2>BoatTracker Daily Report for: {clubInfo.Name}</h2>"); body.AppendLine($"<p>Total reservations: {reservations.Count}</p>"); AddReservationsToReport(body, compliant, "Compliant reservations"); AddReservationsToReport(body, abandoned, "Unused reservations"); AddReservationsToReport(body, failedToCheckOut, "Unclosed reservations"); AddReservationsToReport(body, unknownParticipants, "Incomplete rosters"); AddReservationsToReport(body, withGuest, "Guest rowers"); AddReservationsToReport(body, upcomingWithGuest, "Upcoming guest rowers"); log.Write(body.ToString()); await SendDailyReportEmail( log, clubInfo, $"BoatTracker daily report for {clubInfo.Name}" + (EnvironmentDefinition.Instance.IsDevelopment ? " (DEV)" : string.Empty), body.ToString()); }