You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
4.4 KiB
166 lines
4.4 KiB
package main |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"flag" |
|
"fmt" |
|
"io" |
|
"log" |
|
"net/http" |
|
"net/url" |
|
"os" |
|
"strings" |
|
"time" |
|
|
|
"github.com/BurntSushi/toml" |
|
) |
|
|
|
const DEFAULT_NRDP_BASE_URL = "https://opendata.nationalrail.co.uk" |
|
const DEFAULT_USER_AGENT = "interfare/0.1 (interfare@eta.st)" |
|
|
|
type Config struct { |
|
BaseURL string `toml:"base_url"` |
|
NrdpUsername string `toml:"nrdp_username"` |
|
NrdpPassword string `toml:"nrdp_password"` |
|
UserAgent string `toml:"user_agent"` |
|
} |
|
|
|
func loadConfig(path string) (Config, error) { |
|
var ret Config |
|
data, err := os.ReadFile(path) |
|
if err != nil { |
|
return ret, fmt.Errorf("failed to read config file %s: %w", path, err) |
|
} |
|
_, err = toml.Decode(string(data), &ret) |
|
if err != nil { |
|
return ret, fmt.Errorf("failed to decode config file %s: %w", path, err) |
|
} |
|
if ret.BaseURL == "" { |
|
ret.BaseURL = DEFAULT_NRDP_BASE_URL |
|
} |
|
if ret.UserAgent == "" { |
|
ret.UserAgent = DEFAULT_USER_AGENT |
|
} |
|
if ret.NrdpUsername == "" || ret.NrdpPassword == "" { |
|
return ret, fmt.Errorf("must specify `nrdp_username` and `nrdp_password` in config file") |
|
} |
|
return ret, nil |
|
} |
|
|
|
type NrdpClient struct { |
|
cfg Config |
|
apiToken string |
|
tokenExpiry time.Time |
|
} |
|
|
|
func NewNrdpClient(cfg Config) NrdpClient { |
|
var ret NrdpClient |
|
ret.cfg = cfg |
|
return ret |
|
} |
|
|
|
// EnsureToken checks that the NrdpClient's API token has not expired, and calls /authenticate to get a new one |
|
// if it has. |
|
func (cli *NrdpClient) EnsureToken() error { |
|
if cli.tokenExpiry.After(time.Now()) { |
|
// don't need to do this |
|
return nil |
|
} |
|
uri := fmt.Sprintf("%s/authenticate", cli.cfg.BaseURL) |
|
form := url.Values{} |
|
form.Add("username", cli.cfg.NrdpUsername) |
|
form.Add("password", cli.cfg.NrdpPassword) |
|
req, err := http.NewRequest("POST", uri, strings.NewReader(form.Encode())) |
|
if err != nil { |
|
return fmt.Errorf("failed to build token request: %w", err) |
|
} |
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
|
req.Header.Set("User-Agent", cli.cfg.UserAgent) |
|
resp, err := http.DefaultClient.Do(req) |
|
if err != nil { |
|
return fmt.Errorf("token request failed: %w", err) |
|
} |
|
body := make(map[string]interface{}) |
|
err = json.NewDecoder(resp.Body).Decode(&body) |
|
if err != nil { |
|
return fmt.Errorf("failed to decode token response json: %w", err) |
|
} |
|
token, ok := body["token"] |
|
if !ok { |
|
return fmt.Errorf("token response did not contain token; body %#v", body) |
|
} |
|
tok, ok := token.(string) |
|
if !ok { |
|
return fmt.Errorf("token response was not string; body %#v", body) |
|
} |
|
cli.apiToken = tok |
|
cli.tokenExpiry = time.Now().Add(1 * time.Hour) |
|
return nil |
|
} |
|
|
|
// FetchFile obtains the named file (one of "fares", "routeing", or "timetable") from NRDP |
|
// and returns an interface to read it. |
|
func (cli *NrdpClient) FetchFile(name string) (io.ReadCloser, error) { |
|
if err := cli.EnsureToken(); err != nil { |
|
return nil, fmt.Errorf("failed to ensure token: %w", err) |
|
} |
|
var version string |
|
switch name { |
|
case "routeing": |
|
case "fares": |
|
version = "2.0" |
|
case "timetable": |
|
version = "3.0" |
|
default: |
|
panic(fmt.Sprintf("invalid argument %s to FetchFile", name)) |
|
} |
|
uri := fmt.Sprintf("%s/api/staticfeeds/%s/%s", cli.cfg.BaseURL, version, name) |
|
req, err := http.NewRequest("GET", uri, nil) |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to build file request: %w", err) |
|
} |
|
req.Header.Set("X-Auth-Token", cli.apiToken) |
|
req.Header.Set("User-Agent", cli.cfg.UserAgent) |
|
resp, err := http.DefaultClient.Do(req) |
|
if err != nil { |
|
return nil, fmt.Errorf("file http get failed: %w", err) |
|
} |
|
return resp.Body, nil |
|
} |
|
|
|
var configPath = flag.String("config", "./config.toml", "Path to the configuration file to use.") |
|
|
|
func main() { |
|
flag.Parse() |
|
cfg, err := loadConfig(*configPath) |
|
if err != nil { |
|
log.Fatalf("failed to load config: %s", err) |
|
} |
|
if _, err := os.Stat("./fares.zip"); errors.Is(err, os.ErrNotExist) { |
|
client := NewNrdpClient(cfg) |
|
err = client.EnsureToken() |
|
if err != nil { |
|
log.Fatalf("failed to get token: %s", err) |
|
} |
|
out, err := os.Create("./fares.zip") |
|
if err != nil { |
|
log.Fatalf("failed to create ./fares.zip: %s", err) |
|
} |
|
faresData, err := client.FetchFile("fares") |
|
if err != nil { |
|
log.Fatalf("failed to fetch fares data: %s", err) |
|
} |
|
written, err := io.Copy(out, faresData) |
|
if err != nil { |
|
log.Fatalf("failed to write fares data: %s", err) |
|
} |
|
log.Printf("wrote %d bytes of fares data", written) |
|
} else { |
|
log.Printf("using pre-existing fares.zip") |
|
} |
|
err = ParseFaresData("./fares.zip") |
|
if err != nil { |
|
log.Fatalf("failed to parse fares data: %s", err) |
|
} |
|
}
|
|
|