diff --git a/go/default.nix b/go/default.nix index 237acf88dd..1e9a68741c 100644 --- a/go/default.nix +++ b/go/default.nix @@ -9,7 +9,6 @@ args: { minotarproxy = import ./minotarproxy args; streetworks = import ./streetworks args; trains = import ./trains args; - nhsenglandtests = import ./nhsenglandtests args; journal2clickhouse = import ./journal2clickhouse args; secretsmgr = import ./secretsmgr args; tokend = import ./tokend args; diff --git a/go/nhsenglandtests/default.nix b/go/nhsenglandtests/default.nix deleted file mode 100644 index c859b418a4..0000000000 --- a/go/nhsenglandtests/default.nix +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Luke Granger-Brown -# -# SPDX-License-Identifier: Apache-2.0 - -{ depot, ... }: -depot.third_party.buildGo.program { - name = "nhsenglandtests"; - srcs = [ ./nhsenglandtests.go ]; - deps = with depot.third_party; []; -} diff --git a/go/nhsenglandtests/nhsenglandtests.go b/go/nhsenglandtests/nhsenglandtests.go deleted file mode 100644 index acb9731e26..0000000000 --- a/go/nhsenglandtests/nhsenglandtests.go +++ /dev/null @@ -1,322 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "flag" - "fmt" - "io" - "log" - "net/http" - "os" - "strings" - "time" -) - -type client struct { - http *http.Client -} - -type availabilityType int - -const ( - availabilityUnknown availabilityType = iota - availabilityClosed - availabilityOpen -) - -func (c *client) GetHomeAvailability(ctx context.Context, testType string) (availabilityType, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.test-for-coronavirus.service.gov.uk/ser/app/homeOrderAvailabilityStatus/%s", testType), nil) - if err != nil { - return availabilityUnknown, fmt.Errorf("creating request: %w", err) - } - req.Header.Set("Origin", "https://test-for-coronavirus.service.gov.uk") - req.Header.Set("User-Agent", "TestAvailabilityScraper/1.0 (nhs-test-scraper@lukegb.com)") - req.Header.Set("Accept", "application/json") - resp, err := c.http.Do(req) - if err != nil { - return availabilityUnknown, fmt.Errorf("performing request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return availabilityUnknown, fmt.Errorf("bad HTTP status %d", resp.StatusCode) - } - - type bodyData struct { - Status string `json:"status"` - } - var d bodyData - if err := json.NewDecoder(resp.Body).Decode(&d); err != nil { - return availabilityUnknown, fmt.Errorf("parsing response: %w", err) - } - - switch d.Status { - case "CLOSE": - return availabilityClosed, nil - case "OPEN": - return availabilityOpen, nil - default: - return availabilityUnknown, fmt.Errorf("unknown status %q", d.Status) - } -} - -type homeAvailabilityData struct { - status availabilityType - err error - lastUpdate time.Time - lastSuccess time.Time -} - -func (d *homeAvailabilityData) Clone() *homeAvailabilityData { - return &homeAvailabilityData{ - status: d.status, - err: d.err, - lastUpdate: d.lastUpdate, - lastSuccess: d.lastSuccess, - } -} - -type bot struct { - http *http.Client - token string - targetChannel string - targetPinnedMessage int - tickInterval time.Duration - - nhsClient *client - homeLFDPublic *homeAvailabilityData - homePCRPublic *homeAvailabilityData - homePCRKeyWorker *homeAvailabilityData -} - -func (b *bot) callTelegramNoReply(ctx context.Context, api string, payload interface{}) error { - var payloadBuf bytes.Buffer - if err := json.NewEncoder(&payloadBuf).Encode(payload); err != nil { - return fmt.Errorf("marshalling message payload: %w", err) - } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("https://api.telegram.org/bot%s/%s", b.token, api), &payloadBuf) - if err != nil { - return fmt.Errorf("creating new HTTP request to %v: %w", api, err) - } - req.Header.Set("Content-Type", "application/json") - resp, err := b.http.Do(req) - if err != nil { - return fmt.Errorf("performing %v: %w", api, err) - } - defer resp.Body.Close() - - if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { - return nil - } - io.Copy(os.Stderr, resp.Body) - return fmt.Errorf("%v HTTP status was %d", api, resp.StatusCode) -} - -func (b *bot) editMessageText(ctx context.Context, msgID int, msg string) error { - return b.callTelegramNoReply(ctx, "editMessageText", struct { - ChatID string `json:"chat_id"` - MessageID int `json:"message_id"` - Text string `json:"text"` - ParseMode string `json:"parse_mode"` - DisableWebPagePreview bool `json:"disable_web_page_preview"` - }{ - ChatID: b.targetChannel, - MessageID: msgID, - Text: msg, - ParseMode: "MarkdownV2", - DisableWebPagePreview: true, - }) -} - -func (b *bot) sendMessage(ctx context.Context, msg string) error { - return b.callTelegramNoReply(ctx, "sendMessage", struct { - ChatID string `json:"chat_id"` - Text string `json:"text"` - ParseMode string `json:"parse_mode"` - DisableWebPagePreview bool `json:"disable_web_page_preview"` - }{ - ChatID: b.targetChannel, - Text: msg, - ParseMode: "MarkdownV2", - DisableWebPagePreview: true, - }) -} - -func (b *bot) updateAvailabilityFor(ctx context.Context, outData **homeAvailabilityData, testType string) (bool, error) { - t := time.Now() - avType, err := b.nhsClient.GetHomeAvailability(ctx, testType) - - newData := &homeAvailabilityData{lastUpdate: t} - if *outData != nil { - newData = (*outData).Clone() - newData.lastUpdate = t - } - - if err != nil { - newData.err = err - *outData = newData - return false, fmt.Errorf("updating status for %v: %w", testType, err) - } - - newData.status = avType - newData.err = nil - newData.lastSuccess = t - - updated := *outData == nil || (*outData).status != newData.status - *outData = newData - return updated, nil -} - -type availabilityUpdate struct { - homePCRKeyWorker bool - homePCRPublic bool - homeLFDPublic bool -} - -func (au availabilityUpdate) Any() bool { - return au.homePCRKeyWorker || au.homePCRPublic || au.homeLFDPublic -} - -func (b *bot) updateAvailability(ctx context.Context) (availabilityUpdate, error) { - updatedPCRKW, errPCRKW := b.updateAvailabilityFor(ctx, &b.homePCRKeyWorker, "antigen-keyworkers") - updatedPCRPub, errPCRPub := b.updateAvailabilityFor(ctx, &b.homePCRPublic, "antigen-public") - updatedLFDPub, errLFDPub := b.updateAvailabilityFor(ctx, &b.homeLFDPublic, "lfd3-public") - updated := availabilityUpdate{ - homePCRKeyWorker: updatedPCRKW, - homePCRPublic: updatedPCRPub, - homeLFDPublic: updatedLFDPub, - } - - // If we had only one error, use that. - errs := []error{errPCRKW, errPCRPub, errLFDPub} - errCount := 0 - var lastErr error - allErrsStr := []string{"multiple errors: "} - for _, err := range errs { - if err != nil { - errCount++ - lastErr = err - allErrsStr = append(allErrsStr, err.Error()) - } - } - if errCount == 0 || errCount == 1 { - return updated, lastErr - } - return updated, fmt.Errorf("%v", strings.Join(allErrsStr, "")) -} - -func (b *bot) sendUpdatedAvailability(ctx context.Context, au availabilityUpdate) error { - var msg []string - if au.homePCRKeyWorker && b.homePCRKeyWorker.status == availabilityOpen { - msg = append(msg, "\\[Key workers\\] At\\-home PCR tests are now available\\.") - } - if au.homePCRPublic && b.homePCRPublic.status == availabilityOpen { - msg = append(msg, "\\[Public\\] At\\-home PCR tests are now available\\.") - } - if au.homeLFDPublic && b.homeLFDPublic.status == availabilityOpen { - msg = append(msg, "\\[Public\\] At\\-home LFD tests are now available\\.") - } - if len(msg) == 0 { - return nil - } - - log.Printf("sending updated availability:\n%v", strings.Join(msg, "\n")) - return b.sendMessage(ctx, strings.Join(msg, "\n")) -} - -func (b *bot) updatePinnedMessage(ctx context.Context) error { - format := func(had *homeAvailabilityData, orderURL string) string { - var bits []string - switch had.status { - case availabilityOpen: - bits = append(bits, fmt.Sprintf("[✅ AVAILABLE](%v)", orderURL)) - case availabilityClosed: - bits = append(bits, "❌ out of stock") - case availabilityUnknown: - bits = append(bits, "🤷 unknown") - } - if had.err != nil { - bits = append(bits, "(an error occurred during the last update)") - } - return strings.Join(bits, " ") - } - newMsg := fmt.Sprintf(`Last updated: %v - -At\-home PCR test \(key workers\): %v -At\-home PCR test \(public\): %v -At\-home LFD test \(public\): %v`, - time.Now().Format("2006\\-01\\-02 15\\:04"), - format(b.homePCRKeyWorker, "https://self-referral.test-for-coronavirus.service.gov.uk/antigen/confirm-eligible"), - format(b.homePCRPublic, "https://self-referral.test-for-coronavirus.service.gov.uk/antigen/confirm-eligible"), - format(b.homeLFDPublic, "https://test-for-coronavirus.service.gov.uk/order-lateral-flow-kits"), - ) - log.Printf("updating pinned message:\n%v", newMsg) - return b.editMessageText(ctx, b.targetPinnedMessage, newMsg) -} - -func (b *bot) runOnce(ctx context.Context) { - updated, err := b.updateAvailability(ctx) - if err != nil { - log.Printf("updateAvailability: %v", err) - } - if updated.Any() { - if err := b.sendUpdatedAvailability(ctx, updated); err != nil { - log.Printf("sendUpdatedAvailability: %v", err) - } - } - if err := b.updatePinnedMessage(ctx); err != nil { - log.Printf("updatePinnedMessage: %v", err) - } -} - -func (b *bot) Run(ctx context.Context) error { - t := time.NewTicker(b.tickInterval) - defer t.Stop() - - log.Printf("performing initial run") - b.runOnce(ctx) - log.Printf("here we go") - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-t.C: - log.Print("tick") - b.runOnce(ctx) - } - } -} - -var ( - telegramToken = flag.String("telegram_token", "", "Bot token to use with Telegram Bot API.") - telegramChannel = flag.String("telegram_channel", "@nhsenglandtestavailability", "Channel to send updates to.") - telegramPinned = flag.Int("telegram_pinned_msg_id", 3, "Pinned message to update.") - tickInterval = flag.Duration("tick_interval", 1*time.Minute, "How often to run the main check loop.") -) - -func main() { - flag.Parse() - - if *telegramToken == "" { - log.Fatal("--telegram_token is mandatory") - } - - nhsClient := &client{ - http: new(http.Client), - } - myBot := &bot{ - http: new(http.Client), - token: *telegramToken, - targetChannel: *telegramChannel, - targetPinnedMessage: *telegramPinned, - tickInterval: *tickInterval, - - nhsClient: nhsClient, - } - if err := myBot.Run(context.Background()); err != nil { - log.Fatal(err) - } -} diff --git a/ops/nixos/bvm-nixosmgmt/default.nix b/ops/nixos/bvm-nixosmgmt/default.nix index b553b5aced..fc12276e36 100644 --- a/ops/nixos/bvm-nixosmgmt/default.nix +++ b/ops/nixos/bvm-nixosmgmt/default.nix @@ -6,7 +6,6 @@ { imports = [ ../lib/bvm.nix - ../lib/nhsenglandtests.nix ../../../nix/pkgs/rundeck-bin/module.nix ]; diff --git a/ops/nixos/lib/nhsenglandtests.nix b/ops/nixos/lib/nhsenglandtests.nix deleted file mode 100644 index 6dec4d286e..0000000000 --- a/ops/nixos/lib/nhsenglandtests.nix +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Luke Granger-Brown -# -# SPDX-License-Identifier: Apache-2.0 - -{ pkgs, depot, lib, ... }: -{ - config = { - systemd.services.nhsenglandtests = { - enable = true; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - wants = [ "network.target" ]; - serviceConfig = { - DynamicUser = true; - User = "nhsenglandtests"; - ExecStart = "${depot.go.nhsenglandtests}/bin/nhsenglandtests --telegram_token=${depot.ops.secrets.telegram.nhsenglandtests}"; - }; - }; - }; -}