aboutsummaryrefslogtreecommitdiff
path: root/BartService.cs
blob: 9966a7ee0be182d37d55986fda8b5a4c19f1a34b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
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<BartMember>             BartMembers             { get; set; }
        public DbSet<BartAvailabilityRecord> 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; }
    }

    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 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<BartService> logger,
            IServiceProvider serviceProvider) {

            this.config          = config;
            this.logger          = logger;
            this.serviceProvider = serviceProvider;

            fetchTimer = new() {
                Interval = FetchInterval.TotalMilliseconds
            };
            fetchTimer.Elapsed += OnFetchTimer;
        }

        public async Task FetchDayAsync(int groupId, DateOnly day) {
            using var scope = serviceProvider.CreateScope();
            using var db    = scope.ServiceProvider.GetRequiredService<PagerContext>();

            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  = groupId.ToString(),
                GroupId                    = groupId.ToString(),
                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<BartAvailabilityResponse>())!;
            } 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;
            }

            var existingRecords = db.BartAvailabilityRecords
                .Include(r => r.Member)
                .IntersectBy(users.Select(u => u.BartMemberId), r => r.Member.BartMemberId)
                .OrderByDescending(r => r.Timestamp)
                .GroupBy(r => r.Timestamp)
                .Select(g => g.First())
                .ToArray();

            LinkedList<BartAvailabilityRecord> 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) {
            // TODO: remove hardcoded group ID
            await FetchDayAsync(1366, DateOnly.FromDateTime(DateTime.Now));
        }

        public async Task StartAsync(CancellationToken cancellationToken) {
            logger.LogInformation("BART service starting...");
            loginToken = config.GetValue<string>("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<BartAvailabilityResponseEntry> 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<DateTime> {
            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<DateTime?> {
            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));
            }
        }
    }
}