package broker import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "local/truckstop/config" "net/http" "time" ) type NTGVision struct { searcher interface { search(states []config.State) (io.ReadCloser, error) } } var ErrNoAuth = errors.New("not authorized") type ntgVisionJob struct { ID int64 `json:"id"` PickupDate string `json:"sDate"` PickupCity string `json:"sCity"` PickupState string `json:"sState"` DropoffDate string `json:"cDate"` DropoffCity string `json:"cCity"` DropoffState string `json:"cState"` Miles int `json:"miles"` Weight int `json:"weight"` Equipment string `json:"equip"` Temp string `json:"temp"` } func (ntgJob ntgVisionJob) Job() Job { pickup, _ := time.ParseInLocation("01/02/06", ntgJob.PickupDate, time.Local) dropoff, _ := time.ParseInLocation("01/02/06", ntgJob.DropoffDate, time.Local) return Job{ ID: fmt.Sprintf("ntg-%d", ntgJob.ID), Pickup: JobLocation{ Date: pickup, City: ntgJob.PickupCity, State: ntgJob.PickupState, }, Dropoff: JobLocation{ Date: dropoff, City: ntgJob.DropoffCity, State: ntgJob.DropoffState, }, Miles: ntgJob.Miles, Weight: ntgJob.Weight, Meta: fmt.Sprintf("equipment:%s", ntgJob.Equipment), } } func NewNTGVision() NTGVision { ntgv := NTGVision{} ntgv.searcher = ntgv return ntgv } func (ntg NTGVision) WithMock() NTGVision { ntg.searcher = NewNTGVisionMock() return ntg } func (ntg NTGVision) Search(states []config.State) ([]Job, error) { rc, err := ntg.searcher.search(states) if err != nil { return nil, err } defer rc.Close() var ntgjobs []ntgVisionJob err = json.NewDecoder(rc).Decode(&ntgjobs) jobs := make([]Job, len(ntgjobs)) for i := range jobs { jobs[i] = ntgjobs[i].Job() } return jobs, err } func (ntg NTGVision) search(states []config.State) (io.ReadCloser, error) { if config.Get().Brokers.NTG.Token == "" { if err := ntg.refreshAuth(); err != nil { return nil, err } } rc, err := ntg._search(states) if err == ErrNoAuth { if err := ntg.refreshAuth(); err != nil { return nil, err } rc, err = ntg._search(states) } return rc, err } func (ntg NTGVision) refreshAuth() error { time.Sleep(time.Minute * 2) // TODO b, _ := json.Marshal(map[string]string{ "username": config.Get().Brokers.NTG.Username, "password": config.Get().Brokers.NTG.Password, }) request, err := http.NewRequest(http.MethodPost, "https://ntgvision.com/api/v1/sts/Login", bytes.NewReader(b)) if err != nil { return err } setNTGHeaders(request) resp, err := http.DefaultClient.Do(request) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { b, _ := ioutil.ReadAll(resp.Body) return fmt.Errorf("failed refreshing token: (%d): %s", resp.StatusCode, b) } var v struct { Token string `json:"token"` } if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { return err } if len(v.Token) == 0 { return errors.New("failed to get token from login call") } conf := config.Get() conf.Brokers.NTG.Token = v.Token config.Set(*conf) return nil } func (ntg NTGVision) _search(states []config.State) (io.ReadCloser, error) { request, err := ntg.newRequest(states) if err != nil { return nil, err } c := http.Client{Timeout: time.Minute} resp, err := c.Do(request) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { b, _ := ioutil.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode > 400 && resp.StatusCode < 500 && resp.StatusCode != 404 && resp.StatusCode != 410 { return nil, ErrNoAuth } return nil, fmt.Errorf("bad status searching ntg: %d: %s", resp.StatusCode, b) } return resp.Body, nil } func (ntg NTGVision) newRequest(states []config.State) (*http.Request, error) { body, err := json.Marshal(map[string]interface{}{ "OriginFromDate": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), "OriginToDate": time.Now().UTC().Add(time.Hour * 24 * 30).Format("2006-01-02T15:04:05.000Z"), "DestinationFromDate": nil, "DestinationToDate": nil, "OriginState": states, "OriginRadius": 100, "DestinationState": []string{}, "DestinationRadius": 100, "EquipmentTypes": []string{"STRAIGHT TRUCK", "str truck w/ lift gate"}, "MaxMilesLimit": nil, "MaxNumberOfStopsLimit": nil, "MaxWeightLimit": nil, "MinMilesLimit": nil, "SavedSearchId": 2956, }) if err != nil { return nil, err } request, err := http.NewRequest(http.MethodPost, "https://ntgvision.com/api/v1/load/LoadBoardSearchResults", bytes.NewReader(body)) if err != nil { return nil, err } setNTGHeaders(request) request.Header.Set("Authorization", "Bearer "+config.Get().Brokers.NTG.Token) return request, nil } func setNTGHeaders(request *http.Request) { request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0") request.Header.Set("Accept", "application/json, text/plain, */*") request.Header.Set("Accept-Language", "en-US,en;q=0.5") request.Header.Set("Accept-Encoding", "gzip, deflate, br") request.Header.Set("Content-Type", "application/json;charset=utf-8") //request.Header.Set("Authorization", "Bearer "+config.Get().Brokers.NTG.Token) request.Header.Set("Origin", "https://ntgvision.com") request.Header.Set("DNT", "1") request.Header.Set("Connection", "keep-alive") //request.Header.Set("Cookie", config.Get().Brokers.NTG.Cookie) request.Header.Set("Sec-Fetch-Dest", "empty") request.Header.Set("Sec-Fetch-Mode", "cors") request.Header.Set("Sec-Fetch-Site", "same-origin") request.Header.Set("Pragma", "no-cache") request.Header.Set("Cache-Control", "no-cache") }