📄
BfiScreeningCheckerJob.cs
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
using System.Diagnostics; using BfiMonitor; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Playwright; using Quartz; [DisallowConcurrentExecution] internal sealed class BfiScreeningCheckerJob( PlaywrightBrowserService browserService, ScreeningRepository repository, IOptionsMonitor<MonitorOptions> options, ILogger<BfiScreeningCheckerJob> logger, TimeProvider timeProvider ) : IJob { public async Task Execute(IJobExecutionContext context) { using var activity = Tracing.StartCheckerJob(); var latestHtml = await repository.LatestHtml(context.CancellationToken); logger.LogLatestHtml(latestHtml); var opts = options.CurrentValue; var page = await browserService.NewPageAsync(); try { logger.LogFetchingSite(opts.Url); await page.GotoAsync( opts.Url, new() { WaitUntil = WaitUntilState.DOMContentLoaded, Timeout = TimeSpan.FromSeconds(45).Milliseconds } ); await Task.Delay(TimeSpan.FromSeconds(2)); var resultsElement = await page.WaitForSelectorAsync(opts.Selector); if (resultsElement is null) { logger.LogNoResultsElementFound(opts.Selector); activity?.SetTag("EndReason", "NoResultsElementFound"); activity?.SetStatus(ActivityStatusCode.Ok); return; } var currentHtml = await resultsElement.InnerHTMLAsync(); logger.LogCurrentHtml(currentHtml); if (currentHtml == latestHtml) { logger.LogHtmlNotUpdated(); activity?.SetTag("EndReason", "HtmlNotUpdated"); activity?.SetStatus(ActivityStatusCode.Ok); return; } logger.LogHtmlUpdated(); await repository.InsertNewDetection(currentHtml, timeProvider.GetUtcNow()); var message = $"Site updated: {opts.Url}"; foreach (var phoneNumber in opts.PhoneNumbers) { var dataMap = new JobDataMap { { "phoneNumber", phoneNumber }, { "message", message } }; var trigger = TriggerBuilder.Create().ForJob(SendSmsJob.Key).UsingJobData(dataMap).StartNow().Build(); await context.Scheduler.ScheduleJob(trigger, context.CancellationToken); logger.LogSmsScheduled(phoneNumber); } activity?.SetTag("EndReason", "RanToCompletion"); activity?.SetStatus(ActivityStatusCode.Ok); } catch (Exception ex) { activity?.SetTag("EndReason", "Exception"); activity?.SetStatus(ActivityStatusCode.Error, ex.Message); activity?.AddException(ex); throw; } finally { await page.CloseAsync(); } } } internal static partial class BfiScreeningCheckerJobLoggerExtensions { [LoggerMessage(LogLevel.Information, "Latest HTML detected was {LatestHtml}")] public static partial void LogLatestHtml(this ILogger logger, string? latestHtml); [LoggerMessage(LogLevel.Warning, "No {ElementSelector} element found")] public static partial void LogNoResultsElementFound(this ILogger logger, string elementSelector); [LoggerMessage(LogLevel.Information, "Fetching site {Url}")] public static partial void LogFetchingSite(this ILogger logger, string url); [LoggerMessage(LogLevel.Information, "Current HTML detected is {CurrentHtml}")] public static partial void LogCurrentHtml(this ILogger logger, string? currentHtml); [LoggerMessage(LogLevel.Information, "HTML has not been updated since previous visit")] public static partial void LogHtmlNotUpdated(this ILogger logger); [LoggerMessage(LogLevel.Information, "HTML has been updated since previous visit")] public static partial void LogHtmlUpdated(this ILogger logger); [LoggerMessage(LogLevel.Information, "Scheduled SMS to {PhoneNumber}")] public static partial void LogSmsScheduled(this ILogger logger, string phoneNumber); }