package main import ( "encoding/json" "fmt" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tools/cron" "io" "log" "net/http" "os" "strconv" "time" "traintimes/trains" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" ) type DummyRecord struct { Ts string `json:"ts"` } type GetTrainsRec struct { From string `db:"from" json:"from"` To string `db:"to" json:"to"` Body string `db:"body" json:"body"` Ts int64 `db:"ts" json:"ts"` Hash string `db:"hash" json:"hash"` } type GetTrainTimesRec struct { Hash string `db:"hash" json:"hash"` Body string `db:"body" json:"body"` Ts int64 `db:"ts" json:"ts"` } const host = "https://huxley2.azurewebsites.net" var ( Version string Build string tr = &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: true, } client = &http.Client{Transport: tr} gmtTimeLoc = time.FixedZone("GMT", 0) timeFormat = "Mon, 2 Jan 2006 15:04:05 GMT" ) func main() { log.Printf("GO-TRAINTIMES v%+v build %+v\n\n", Version, Build) app := pocketbase.New() // serves static files from the provided public dir (if exists) app.OnBeforeServe().Add(func(e *core.ServeEvent) error { scheduler := cron.New() e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), false)) e.Router.GET("/gettrains", func(c echo.Context) error { apis.ActivityLogger(app) return getTrains(c, app) }) e.Router.GET("/getnexttraintimes", func(c echo.Context) error { apis.ActivityLogger(app) return GetNextTrainTimes(c, app) }) scheduler.MustAdd("cleanup", "*/10 * * * *", func() { go CleanupDB(app) }) scheduler.Start() return nil }) if err := app.Start(); err != nil { log.Fatal(err) } } func NotImplemented(c echo.Context, from string) error { params := c.QueryParams() fmt.Printf("NotImplemented from: %s\n", from) fmt.Printf("-- %+v\n", params) return nil } func getTrains(c echo.Context, app *pocketbase.PocketBase) error { params := c.QueryParams() fmt.Printf("$$:getTrains") fmt.Printf("-- %+v\n", params) From := c.QueryParamDefault("from", "") To := c.QueryParamDefault("to", "") ts := time.Now().Unix() log.Printf("-- %+v\n", ts) log.Printf("-- From: %s\n", From) log.Printf("-- To: %s\n", To) dummy := DummyRecord{Ts: strconv.FormatInt(ts, 10)} if From != "" && To != "" { log.Println("-- Got something to search for") // check db first.. recentTrain := GetTrainsRec{} hash := fmt.Sprintf("%s%s", From, To) dberr := app.Dao().DB().NewQuery("SELECT * FROM trains WHERE hash = {:hash} and ts >= {:ts}").Bind(dbx.Params{ "ts": ts - 120, "hash": hash, }).One(&recentTrain) if dberr == nil { // handle error log.Printf("-- Cache hit train %+v\n", recentTrain.Hash) return c.String(200, recentTrain.Body) } // `/all/${ req.query.from }/to/${ req.query.to }/10?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8`; url := "/all/" + From + "/to/" + To + "/10?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8" log.Printf("-- url: %+v\n", url) /*tr := &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: true, }*/ // client := &http.Client{Transport: tr} log.Printf("-- full url: %+v\n", host+url) resp, err := client.Get(host + url) if err != nil { // handle error } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } var nTrainRec GetTrainsRec nTrainRec.To = To nTrainRec.From = From nTrainRec.Ts = ts nTrainRec.Body = string(body) nTrainRec.Hash = hash go func() { err := saveTrainRec(app, nTrainRec) if err != nil { log.Println(err) } }() // log.Printf("-- body: %+v\n", string(body)) // return c.SendString(string(body)) return c.String(200, string(body)) } return c.JSON(http.StatusAccepted, dummy) } func GetNextTrainTimes(c echo.Context, app *pocketbase.PocketBase) error { params := c.QueryParams() log.Println("$$:GetNextTrainTimes") log.Printf("-- %+v\n", params) From := c.QueryParamDefault("from", "") To := c.QueryParamDefault("to", "") ts := time.Now().String() tsUnix := time.Now().Unix() now := time.Now() then := now.Add(-2 * time.Minute) log.Printf("-- %+v\n", ts) log.Printf("-- From: %+v\n", From) log.Printf("-- To: %+v\n", To) dummy := DummyRecord{Ts: ts} recentTraintimes := GetTrainTimesRec{} hash := fmt.Sprintf("%s%s", From, To) dberr := app.Dao().DB().NewQuery("SELECT * FROM traintimes WHERE hash = {:hash} and ts >= {:ts}").Bind(dbx.Params{ "ts": tsUnix - 120, "hash": hash, }).One(&recentTraintimes) if dberr == nil { // handle error log.Printf("-- Cache hit traintimes %+v\n", recentTraintimes.Hash) return c.String(200, recentTraintimes.Body) } if From != "" && To != "" { log.Println("-- Got something to search for") // `/all/${ req.query.from }/to/${ req.query.to }/10?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8`; url := "/next/" + From + "/to/" + To + "?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8" log.Printf("-- url: %+v\n", url) log.Printf("-- full url: %+v\n", host+url) req, err := http.NewRequest("GET", host+url, nil) if err != nil { // handle error } req.Header.Add("If-Modified-Since", then.In(gmtTimeLoc).Format(timeFormat)) resp, err := client.Do(req) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } var services trains.AllDepartures unerr := json.Unmarshal(body, &services) if unerr != nil { log.Println("Failed to Unmarshal json") panic(unerr) } departure := reduceNextTrainTimes(services) departJson, derr := json.Marshal(departure) if derr != nil { log.Println("Failed to marshal json") panic(derr) } var nTrainTimesRec GetTrainTimesRec nTrainTimesRec.Hash = hash nTrainTimesRec.Ts = tsUnix nTrainTimesRec.Body = string(departJson) go func() { err := saveTraintimesRec(app, nTrainTimesRec) if err != nil { log.Println(err) } }() // log.Println("-- body: %+v\n", string(body)) // c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) // return c.SendString(string(departJson)) // return c.Send(body) return c.String(200, string(departJson)) } return c.JSON(http.StatusAccepted, dummy) } func reduceNextTrainTimes(departData trains.AllDepartures) trains.NextTrain { // log.Printf("-- obj?: %+v\n", departData) depart := departData.Departures[0].Service // log.Printf("-- depart: %+v\n", depart) var output trains.NextTrain if depart.Origin != nil { if depart.Sta != "" { output.Sta = depart.Sta } else { output.Sta = depart.Std } if depart.Eta != "" { output.Eta = depart.Eta } else { output.Eta = depart.Etd } } else { output.Eta = "No Service" output.Sta = "No Service" } // log.Printf("-- output: %+v\n", depart) return output } func saveTrainRec(app *pocketbase.PocketBase, trainRecord GetTrainsRec) error { log.Println("$$:saveTrainRec") // foundTrain := GetTrainsRec{} collection, err := app.Dao().FindCollectionByNameOrId("trains") if err != nil { log.Println(err) return err } record, recErr := app.Dao().FindRecordsByExpr("trains", dbx.NewExp("hash = {:hash}", dbx.Params{ "hash": trainRecord.Hash, })) if recErr != nil { log.Println(recErr) return recErr } if len(record) == 0 { log.Printf("-- Insert new train\n") record := models.NewRecord(collection) record.Set("from", trainRecord.From) record.Set("to", trainRecord.To) record.Set("body", trainRecord.Body) record.Set("ts", trainRecord.Ts) record.Set("hash", trainRecord.Hash) if err := app.Dao().SaveRecord(record); err != nil { log.Println(err) return err } } else { log.Printf("-- Update train\n") rec := record[0] rec.Set("body", trainRecord.Body) rec.Set("ts", trainRecord.Ts) if err := app.Dao().SaveRecord(rec); err != nil { log.Println(err) return err } } /*dberr := app.Dao().DB().NewQuery("SELECT * FROM trains WHERE hash = {:hash}").Bind(dbx.Params{ "hash": trainRecord.Hash, }).One(&foundTrain) if dberr != nil { // handle error log.Printf("-- Insert New Record %+v,%+v\n", trainRecord.Hash) record := models.NewRecord(collection) record.Set("from", trainRecord.From) record.Set("to", trainRecord.To) record.Set("body", trainRecord.Body) record.Set("ts", trainRecord.Ts) record.Set("hash", trainRecord.Hash) if err := app.Dao().SaveRecord(record); err != nil { log.Println(err) return err } } else { foundTrain.Ts = trainRecord.Ts foundTrain.Body = trainRecord.Body record := models.NewRecord(foundTrain) if err := app.Dao().SaveRecord(record); err != nil { log.Println(err) return err } }*/ return nil } func saveTraintimesRec(app *pocketbase.PocketBase, traintimesRecord GetTrainTimesRec) error { log.Println("$$:saveTraintimesRec") // foundTrain := GetTrainsRec{} collection, err := app.Dao().FindCollectionByNameOrId("traintimes") if err != nil { log.Println(err) return err } record, recErr := app.Dao().FindRecordsByExpr("traintimes", dbx.NewExp("hash = {:hash}", dbx.Params{ "hash": traintimesRecord.Hash, })) if recErr != nil { log.Println(recErr) return recErr } if len(record) == 0 { log.Printf("-- Insert new traintimes\n") record := models.NewRecord(collection) record.Set("body", traintimesRecord.Body) record.Set("ts", traintimesRecord.Ts) record.Set("hash", traintimesRecord.Hash) if err := app.Dao().SaveRecord(record); err != nil { log.Println(err) return err } } else { log.Printf("-- Update traintimes\n") rec := record[0] rec.Set("body", traintimesRecord.Body) rec.Set("ts", traintimesRecord.Ts) if err := app.Dao().SaveRecord(rec); err != nil { log.Println(err) return err } } return nil } func CleanupDB(app *pocketbase.PocketBase) { ts := time.Now().Unix() - 600 log.Println("$$:CleanupDB", ts) /*raw := "Delete from trains where ts <=" + strconv.FormatInt(ts, 10) if _, err := app.Dao().DB().NewQuery(raw).Execute(); err != nil { log.Fatalln(err) }*/ }