2020-04-25 02:36:05 +00:00
package main
import (
"bufio"
"encoding/json"
2020-04-25 07:09:15 +00:00
"flag"
2020-04-25 02:36:05 +00:00
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
2020-04-25 07:09:15 +00:00
"os/signal"
2020-04-25 02:36:05 +00:00
"strings"
2020-04-25 07:09:15 +00:00
"text/template"
"time"
2020-04-25 02:36:05 +00:00
)
const (
2020-04-25 07:09:15 +00:00
statsURL = "https://covid19stats.alberta.ca"
cacheTimeout = time . Hour * 8
2020-04-25 02:36:05 +00:00
)
2020-04-25 07:09:15 +00:00
var (
cache * Cache
t * template . Template // HTML Templates used for web mode. Initialized as a null pointer has near zero overhead when cli mode is run
)
// ABGovCovidData stores the Javascript object which is ripped from the statsURL. It's parsed as JSON encoded data.
type ABGovCovidData [ ] [ ] string
2020-04-25 02:36:05 +00:00
// They screwed this up so much, the first index in the column, not the row. WTF alberta
// [0] = case number alberta
// [1] = date
// [2] = geo zone
// [3] = gender
// [4] = age range
// [5] = case status
// [6] = confirmed/suspected
2020-04-25 07:09:15 +00:00
// Cache keeps a local copy of the data in memory. The cache will only update if a web request comes in, not on a fixed timer.
// This is to ensure reduced load on Alberta's servers and to ensure a quick response when possible, and err on the side of caution when not possible.
type Cache struct {
Data struct {
ActiveCasesEdmonton int
TotalCasesEdmonton int
ActiveCasesAlberta int
TotalCasesAlberta int
}
UpdatedDate time . Time
}
2020-04-25 02:36:05 +00:00
func main ( ) {
2020-04-25 07:09:15 +00:00
webMode := flag . Bool ( "web" , false , "listen on port 8080 as web server" )
flag . Parse ( )
if * webMode {
// Setup a web server
stop := make ( chan os . Signal , 1 )
signal . Notify ( stop , os . Interrupt )
// Initial load of data
go func ( ) {
for {
var err error
cache , err = getUpdatedData ( )
if err != nil {
log . Printf ( "cache is empty and failed to download data from ab website: %v" , err )
fmt . Println ( "sleeping for 2 hours" )
time . Sleep ( time . Hour * 2 )
continue
}
break
}
} ( )
// Web server routing
go func ( ) {
http . Handle ( "/public/" , http . StripPrefix ( "/public/" , http . FileServer ( http . Dir ( "public" ) ) ) )
http . HandleFunc ( "/" , homePageHandler )
log . Fatal ( http . ListenAndServe ( ":8080" , nil ) )
} ( )
log . Println ( "started covid19-edmonton server on 8080" )
2020-04-25 02:36:05 +00:00
2020-04-25 07:09:15 +00:00
// Parse all template files at startup
var err error
t , err = template . ParseGlob ( "./templates/*" )
if err != nil {
log . Fatalf ( "couldn't parse HTML templates in templates directory: %v" , err )
}
<- stop
log . Println ( "shutting covid19-edmonton server down" )
} else {
var err error
cache , err = getUpdatedData ( )
if err != nil {
log . Fatalf ( "failed to get data from alberta government website: %v" , err )
}
// CLI mode, print output to stdout
fmt . Println ( "Edmonton Active: " , cache . Data . ActiveCasesEdmonton )
fmt . Println ( "Edmonton Total: " , cache . Data . TotalCasesEdmonton )
fmt . Println ( "Alberta Active: " , cache . Data . ActiveCasesAlberta )
fmt . Println ( "Alberta Total: " , cache . Data . TotalCasesAlberta )
log . Print ( "\n\nPress 'Enter' to continue..." )
bufio . NewReader ( os . Stdin ) . ReadBytes ( '\n' )
}
}
// getUpdatedData reaches out to Alberta's website to get the most recent data and returns a pointer to a cache and an error
func getUpdatedData ( ) ( * Cache , error ) {
log . Printf ( "getting up to date data from AB website" )
2020-04-25 02:36:05 +00:00
// Download the latest stats
resp , err := http . Get ( statsURL )
if err != nil {
2020-04-25 07:09:15 +00:00
return nil , fmt . Errorf ( "failed to download stats page '%s': %v" , statsURL , err )
2020-04-25 02:36:05 +00:00
}
defer resp . Body . Close ( )
bodyB , err := ioutil . ReadAll ( resp . Body )
if err != nil {
2020-04-25 07:09:15 +00:00
return nil , fmt . Errorf ( "failed to read stats page response body: %v" , err )
2020-04-25 02:36:05 +00:00
}
// Parse the HTML for the data
// Find the beginning, trim off everything before
split := strings . Split ( string ( bodyB ) , "\"data\":[[\"1\"," )
body := fmt . Sprintf ( "%s%s" , "[[\"1\"," , split [ 1 ] )
// Find the end, trim off everything after
split = strings . Split ( body , "]]" )
body = fmt . Sprintf ( "%s%s" , split [ 0 ] , "]]" )
// Parse the json string into a struct
2020-04-25 07:09:15 +00:00
data := ABGovCovidData { }
2020-04-25 02:36:05 +00:00
err = json . Unmarshal ( [ ] byte ( body ) , & data )
if err != nil {
2020-04-25 07:09:15 +00:00
return nil , fmt . Errorf ( "failed to parse data as json: %v" , err )
2020-04-25 02:36:05 +00:00
}
2020-04-25 07:09:15 +00:00
cache := & Cache { UpdatedDate : time . Now ( ) }
2020-04-25 02:36:05 +00:00
// count the cases
for i := range data [ 2 ] {
if data [ 2 ] [ i ] == "Edmonton Zone" {
2020-04-25 07:09:15 +00:00
cache . Data . TotalCasesEdmonton ++
2020-04-25 02:36:05 +00:00
if data [ 5 ] [ i ] == "Active" {
2020-04-25 07:09:15 +00:00
cache . Data . ActiveCasesEdmonton ++
2020-04-25 02:36:05 +00:00
}
}
if data [ 5 ] [ i ] == "Active" {
2020-04-25 07:09:15 +00:00
cache . Data . ActiveCasesAlberta ++
2020-04-25 02:36:05 +00:00
}
2020-04-25 07:09:15 +00:00
cache . Data . TotalCasesAlberta ++
2020-04-25 02:36:05 +00:00
}
2020-04-25 07:09:15 +00:00
return cache , nil
}
// Web Mode only: HTTP GET /
func homePageHandler ( w http . ResponseWriter , r * http . Request ) {
if r . Method == "GET" {
if cache == nil {
w . WriteHeader ( http . StatusFailedDependency )
log . Printf ( "unable to serve page due to no cached data, possibly due to application still starting up, or AB government site down, or changed formatting - may need to review how page is parsed" )
return
}
if time . Now ( ) . After ( cache . UpdatedDate . Add ( cacheTimeout ) ) {
w . WriteHeader ( http . StatusOK )
tempCache , err := getUpdatedData ( ) // Hold tempCache in case there's an error, we don't want to nullify our pointer to a working cache that has aged. We will proceed with aged data.
if err != nil {
log . Printf ( "failed to update cache: %v" , err )
} else {
cache = tempCache // we retrieved it successfully, so overwrite our old data
}
}
2020-04-25 02:36:05 +00:00
2020-04-25 07:09:15 +00:00
err := t . ExecuteTemplate ( w , "index.html" , cache )
if err != nil {
w . WriteHeader ( http . StatusInternalServerError )
log . Fatalf ( "failed to execute index.html template: %v" , err )
}
2020-04-25 02:36:05 +00:00
2020-04-25 07:09:15 +00:00
} else {
w . WriteHeader ( http . StatusMethodNotAllowed )
}
2020-04-25 02:36:05 +00:00
}