using Microsoft.EntityFrameworkCore; using PagerParser.Bart; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Serialization; using System.Timers; namespace PagerParser { public partial class PagerContext { public DbSet BartMembers { get; set; } public DbSet BartAvailabilityRecords { get; set; } } } namespace PagerParser.Bart { public enum BartStatus { NotAvailable = -1, Clear = 0, Available = 1, AtPremises = 2, EmergencyOnly = 3, Delayed = 4 } [Index(nameof(MemberName))] public record BartMember { [JsonIgnore] [Key] public int BartMemberId { get; set; } public string MemberName { get; set; } } [Index(nameof(Hashcode))] public record BartAvailabilityRecord { [JsonIgnore] [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int BartAvailabilityRecordId { get; set; } public DateTime Timestamp { get; set; } public BartMember Member { get; set; } public BartStatus Status { get; set; } public bool IsDefault { get; set; } public DateTime? ModifiedOn { get; set; } public BartMember? ModifiedBy { get; set; } public DateTime? CreatedOn { get; set; } public BartMember? CreatedBy { get; set; } public Guid Hashcode { get { int?[] hashes = [ Timestamp.GetHashCode(), Member.BartMemberId.GetHashCode(), Status.GetHashCode(), IsDefault.GetHashCode(), ModifiedOn.GetHashCode(), ModifiedBy?.BartMemberId.GetHashCode(), CreatedOn.GetHashCode(), CreatedBy?.BartMemberId.GetHashCode() ]; var bytes = hashes .Where(x => x is not null) .Cast() .SelectMany(BitConverter.GetBytes) .ToArray(); using var md5 = MD5.Create(); return new Guid(md5.ComputeHash(bytes)); } set => _ = value; } } public class BartService : IHostedService { private const string AvailabilityEndpointUrl = @"https://bartapp.net/webapp/webservice/WebsiteService.svc/GetUserAvailabilityList"; private readonly TimeSpan FetchInterval = TimeSpan.FromMinutes(5); private IConfiguration config; private ILogger logger; private IServiceProvider serviceProvider; private string loginToken; private HttpClient httpClient; private System.Timers.Timer fetchTimer; public BartService( IConfiguration config, ILogger logger, IServiceProvider serviceProvider) { this.config = config; this.logger = logger; this.serviceProvider = serviceProvider; fetchTimer = new() { Interval = FetchInterval.TotalMilliseconds }; fetchTimer.Elapsed += OnFetchTimer; } public async Task FetchDayAsync(DateOnly day) { using var scope = serviceProvider.CreateScope(); using var db = scope.ServiceProvider.GetRequiredService(); logger.LogDebug($"Fetching BART availability status for {day}"); // Prepare API request var request = new BartAvailabilityRequest() { RelatedDay = day.ToString("yyyy-MM-dd"), LoginToken = loginToken, PermissionLevel = 2, UserQualFilterIds = [], AvailabilityGroupFilterId = "1366", GroupId = "1366", Length = 0 }; // Fetch the data HttpResponseMessage response; try { response = await httpClient.PostAsJsonAsync( AvailabilityEndpointUrl, request, new JsonSerializerOptions() { PropertyNamingPolicy = null }); } catch(Exception e) { logger.LogError(e, "Error fetching availability status from BART"); return; } BartAvailabilityResponse responseData; try { responseData = (await response.Content .ReadFromJsonAsync())!; } catch(Exception e) { logger.LogError(e, "Error parsing BART JSON response"); return; } // Parse out a list of users from the response var users = responseData.GetUserAvailabilityListResult.List .Select(e => new BartMember() { BartMemberId = e.UserId, MemberName = e.MemberName.Trim() }) .ToArray(); var existingUsers = db.BartMembers .Select(m => m.BartMemberId) .ToArray(); // Add new users to the DB and update the names of existing ones try { foreach(var user in users) { if(existingUsers.Contains(user.BartMemberId)) db.Update(user); else db.Add(user); } await db.SaveChangesAsync(); } catch(Exception e) { logger.LogError(e, "Error saving BART members to the DB"); return; } LinkedList availabilityRecords = new(); foreach(var entry in responseData.GetUserAvailabilityListResult.List) { try { for(int hour = 0; hour < 24; hour++) { var timestamp = new DateTime( DateOnly.FromDateTime(entry.RelatedDay), new TimeOnly(hour, 0), DateTimeKind.Local); var status = (BartStatus) entry.GetType() .GetProperty($"Block{hour + 1}")! .GetValue(entry)!; var record = new BartAvailabilityRecord() { Timestamp = timestamp.ToUniversalTime(), Member = users.First(u => u.BartMemberId == entry.UserId), Status = status, IsDefault = entry.AvailabilityId < 0, ModifiedOn = entry.ModifiedOn, ModifiedBy = users.FirstOrDefault(u => u.MemberName == entry.ModifiedBy), CreatedBy = users.FirstOrDefault(u => u.MemberName == entry.CreatedBy) }; // Optionally set the create/modify time if // one is present in the fetched record if(entry.ModifiedOn is not null) record.ModifiedOn = DateTime.SpecifyKind((DateTime) entry.ModifiedOn, DateTimeKind.Utc); if(entry.CreatedOn is not null) record.CreatedOn = DateTime.SpecifyKind((DateTime) entry.CreatedOn, DateTimeKind.Utc); availabilityRecords.AddLast(record); } } catch(Exception e) { logger.LogError(e, "Unknown error occurred whilst parsing availability entries"); return; } } var toAdd = availabilityRecords .ExceptBy(db.BartAvailabilityRecords.Select(e => e.Hashcode), e => e.Hashcode); try { await db.BartAvailabilityRecords.AddRangeAsync(toAdd); await db.SaveChangesAsync(); } catch(Exception e) { logger.LogError(e, "Error committing availability entries to the DB"); return; } } private async void OnFetchTimer(object? sender, ElapsedEventArgs e) { await FetchDayAsync(DateOnly.FromDateTime(DateTime.Now)); } public async Task StartAsync(CancellationToken cancellationToken) { logger.LogInformation("BART service starting..."); loginToken = config.GetValue("PagerParser:Bart:Token"); httpClient = new(); fetchTimer.Start(); } public Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("BART service stopping..."); fetchTimer.Stop(); loginToken = null; httpClient.Dispose(); return Task.CompletedTask; } private record BartAvailabilityRequest { public string RelatedDay { get; set; } public string LoginToken { get; set; } public int PermissionLevel { get; set; } public string[] UserQualFilterIds { get; set; } public string AvailabilityGroupFilterId { get; set; } public string GroupId { get; set; } [JsonPropertyName("length")] public int Length { get; set; } } private record BartAvailabilityResponse { public BartAvailabilityResponseResult GetUserAvailabilityListResult { get; set; } } private record BartAvailabilityResponseResult { public List List { get; set; } } private record BartAvailabilityResponseEntry { public int AvailabilityId { get; set; } [JsonConverter(typeof(BartDateTimeConverter))] public DateTime RelatedDay { get; set; } public string MemberName { get; set; } [JsonConverter(typeof(BartNullableDateTimeConverter))] public DateTime? CreatedOn { get; set; } public string? CreatedBy { get; set; } [JsonConverter(typeof(BartNullableDateTimeConverter))] public DateTime? ModifiedOn { get; set; } public string? ModifiedBy { get; set; } public int UserId { get; set; } public int Block1 { get; set; } public int Block2 { get; set; } public int Block3 { get; set; } public int Block4 { get; set; } public int Block5 { get; set; } public int Block6 { get; set; } public int Block7 { get; set; } public int Block8 { get; set; } public int Block9 { get; set; } public int Block10 { get; set; } public int Block11 { get; set; } public int Block12 { get; set; } public int Block13 { get; set; } public int Block14 { get; set; } public int Block15 { get; set; } public int Block16 { get; set; } public int Block17 { get; set; } public int Block18 { get; set; } public int Block19 { get; set; } public int Block20 { get; set; } public int Block21 { get; set; } public int Block22 { get; set; } public int Block23 { get; set; } public int Block24 { get; set; } } private class BartDateTimeConverter : JsonConverter { private const string Format = "yyyy-MM-dd HH:mm:ss"; public override DateTime Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return DateTime.ParseExact(reader.GetString()!, Format, null); } public override void Write( Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString(Format)); } } private class BartNullableDateTimeConverter : JsonConverter { private const string Format = "yyyy-MM-dd HH:mm:ss"; public override DateTime? Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { string? value = reader.GetString(); if(string.IsNullOrEmpty(value)) return null; return DateTime.ParseExact(value, Format, null); } public override void Write( Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) { writer.WriteStringValue(value?.ToString(Format)); } } } }