nhsenglandtests: delete
This commit is contained in:
parent
8407c1a743
commit
35a9ec6bf5
5 changed files with 0 additions and 354 deletions
|
@ -9,7 +9,6 @@ args: {
|
||||||
minotarproxy = import ./minotarproxy args;
|
minotarproxy = import ./minotarproxy args;
|
||||||
streetworks = import ./streetworks args;
|
streetworks = import ./streetworks args;
|
||||||
trains = import ./trains args;
|
trains = import ./trains args;
|
||||||
nhsenglandtests = import ./nhsenglandtests args;
|
|
||||||
journal2clickhouse = import ./journal2clickhouse args;
|
journal2clickhouse = import ./journal2clickhouse args;
|
||||||
secretsmgr = import ./secretsmgr args;
|
secretsmgr = import ./secretsmgr args;
|
||||||
tokend = import ./tokend args;
|
tokend = import ./tokend args;
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2020 Luke Granger-Brown <depot@lukegb.com>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
{ depot, ... }:
|
|
||||||
depot.third_party.buildGo.program {
|
|
||||||
name = "nhsenglandtests";
|
|
||||||
srcs = [ ./nhsenglandtests.go ];
|
|
||||||
deps = with depot.third_party; [];
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../lib/bvm.nix
|
../lib/bvm.nix
|
||||||
../lib/nhsenglandtests.nix
|
|
||||||
../../../nix/pkgs/rundeck-bin/module.nix
|
../../../nix/pkgs/rundeck-bin/module.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2020 Luke Granger-Brown <depot@lukegb.com>
|
|
||||||
#
|
|
||||||
# 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}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
Loading…
Reference in a new issue