diff --git a/.gitignore b/.gitignore index ab4ff49..e45c6bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.swp *.swo *.DS_Store +*.exe + diff --git a/api/api.go b/api/api.go index d7c63e0..c6fef9d 100644 --- a/api/api.go +++ b/api/api.go @@ -7,6 +7,7 @@ package api import ( "errors" "strconv" + "time" ) var ( @@ -15,6 +16,8 @@ var ( ErrNetwork = errors.New("unknown network error") ErrPastDate = errors.New("latest reservation time has passed") ErrTimeNull = errors.New("times list empty") + ErrNoOffer = errors.New("table is not offered on given date") + ErrNoPayInfo = errors.New("no payment info on account") ) /* @@ -96,12 +99,27 @@ type SearchResult struct { /* Name: Time Type: API Input Struct -Purpose: Provide a go indepent struct for representing time +Purpose: Provide a go independent struct for representing time */ +/* type Time struct { + CTime time.Time +} +*/ + +/* +Name: LongTime +Type: API Input Struct +Purpose: Provide a go indepent struct for representing time +at a long scale(i.e. years + months + days) +*/ +/*type LongTime struct { + Year string + Month string + Day string Hour string Minute string -} +}*/ /* Name: ReserveParam @@ -110,10 +128,7 @@ Purpose: Input information to the 'Reserve' api function */ type ReserveParam struct { VenueID int64 - Day string - Month string - Year string - ReservationTimes []Time + ReservationTimes []time.Time PartySize int LoginResp LoginResponse } @@ -124,7 +139,7 @@ Type: API Func Output Struct Purpose: Output information from the 'Reserve' api function */ type ReserveResponse struct { - ReservationTime Time + ReservationTime time.Time } /* @@ -138,6 +153,7 @@ type API interface { Login(params LoginParam) (*LoginResponse, error) Search(params SearchParam) (*SearchResponse, error) Reserve(params ReserveParam) (*ReserveResponse, error) + AuthMinExpire() (time.Duration) } /* diff --git a/api/doc.go b/api/doc.go index 854ed97..a32b81e 100644 --- a/api/doc.go +++ b/api/doc.go @@ -56,5 +56,15 @@ Search: reservation request. ********************************************************************** + +AuthMinExpire: + + The AuthMinExpire function provides the minimum time irresepective + of time zone that a login token from the Login function is valid. + This function returns a constant value. If a null value is + returned, the login token is valid indefinitely. + +********************************************************************** + */ package api diff --git a/api/resy/api.go b/api/resy/api.go index 6014076..9b2c6a0 100644 --- a/api/resy/api.go +++ b/api/resy/api.go @@ -13,6 +13,7 @@ import ( "bytes" "strconv" "strings" + "time" ) /* @@ -124,16 +125,23 @@ func (a *API) Login(params api.LoginParam) (*api.LoginResponse, error) { defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) + if err != nil { return nil, err } + var jsonMap map[string]interface{} err = json.Unmarshal(responseBody, &jsonMap) if err != nil { return nil, err } + if jsonMap["payment_method_id"] == nil { + return nil, api.ErrNoPayInfo + } + + loginResponse := api.LoginResponse{ ID: int64(jsonMap["id"].(float64)), FirstName: jsonMap["first_name"].(string), @@ -236,7 +244,10 @@ Purpose: Resy implementation of the Reserve api func func (a *API) Reserve(params api.ReserveParam) (*api.ReserveResponse, error) { // converting fields to url query format - date := params.Year + "-" + params.Month + "-" + params.Day + year := strconv.Itoa(params.ReservationTimes[0].Year()) + month := strconv.Itoa(int(params.ReservationTimes[0].Month())) + day := strconv.Itoa(params.ReservationTimes[0].Day()) + date := year + "-" + month + "-" + day dayField := `day=` + date authField := `x-resy-auth-token=` + params.LoginResp.AuthToken latField := `lat=0` @@ -282,12 +293,16 @@ func (a *API) Reserve(params api.ReserveParam) (*api.ReserveResponse, error) { return nil, err } + // JSON structure is complicated here, see api/resy/doc.go for full explanation jsonResultsMap := jsonTopLevelMap["results"].(map[string]interface{}) - jsonVenuesList := jsonResultsMap["venues"].([]interface{}) + jsonVenuesList := jsonResultsMap["venues"].([]interface{}) + if len(jsonVenuesList) == 0 { + return nil, api.ErrNoOffer + } jsonVenueMap := jsonVenuesList[0].(map[string]interface{}) jsonSlotsList := jsonVenueMap["slots"].([]interface{}) - for i:=0; i < len(params.ReservationTimes); i++ { + for i := 0; i < len(params.ReservationTimes); i++ { currentTime := params.ReservationTimes[i] for j:=0; j < len(jsonSlotsList); j++ { @@ -302,8 +317,19 @@ func (a *API) Reserve(params api.ReserveParam) (*api.ReserveResponse, error) { startFields := strings.Split(startRaw, " ") // isolate time field and split to get ["HrHr","MnMn"] timeFields := strings.Split(startFields[1], ":") - // if time field matches of slot matches current selected ResTime, move to config step - if timeFields[0] == currentTime.Hour && timeFields[1] == currentTime.Minute { + // if time field matches of slot matches current selected ResTime, move to config step + + hourFieldInt, err := strconv.Atoi(timeFields[0]) + if err != nil { + return nil, err + } + + minFieldInt, err := strconv.Atoi(timeFields[1]) + if err != nil { + return nil, err + } + + if hourFieldInt == currentTime.Hour() && minFieldInt == currentTime.Minute() { jsonConfigMap := jsonSlotMap["config"].(map[string]interface{}) configToken := jsonConfigMap["token"].(string) configIDField := `config_id=` + url.QueryEscape(configToken) @@ -395,6 +421,18 @@ func (a *API) Reserve(params api.ReserveParam) (*api.ReserveResponse, error) { return nil, api.ErrNoTable } +/* +Name: AuthMinExpire +Type: API Func +Purpose: Resy implementation of the AuthMinExpire api func. +The largest minimum validity time is 6 days. +*/ +func (a *API) AuthMinExpire() (time.Duration) { + /* 6 days */ + var d time.Duration = time.Hour * 24 * 6 + return d +} + //func (a *API) Cancel(params api.CancelParam) (*api.CancelResponse, error) { // cancelUrl := `https://api.resy.com/3/cancel` // resyToken := url.QueryEscape(params.ResyToken) diff --git a/api/resy/doc.go b/api/resy/doc.go index 4e2f19b..556a390 100644 --- a/api/resy/doc.go +++ b/api/resy/doc.go @@ -36,7 +36,7 @@ General Overview of Resy API: The Search functionality of Resy requires only one request message, and generally takes a query name as input. On success, we are - given a JSON of restaurant data entries matching the query in + given a JSON of restaurant data entries matching the query in some way. The specific message structure of a Search operation is discussed in the 'Search' section of this document. diff --git a/app/app.go b/app/app.go index e4d7ddf..f566ba5 100644 --- a/app/app.go +++ b/app/app.go @@ -9,6 +9,7 @@ import ( "errors" "time" "strconv" + "fmt" ) var ( @@ -71,12 +72,9 @@ reserve at interval operation by a consumer type ReserveAtIntervalParam struct { Login LoginParam VenueID int64 - Day string - Month string - Year string - ReservationTimes []api.Time + ReservationTimes []time.Time PartySize int - RepeatInterval api.Time + RepeatInterval time.Duration } /* @@ -88,15 +86,9 @@ reserve at time operation by a consumer type ReserveAtTimeParam struct { Login LoginParam VenueID int64 - Day string - Month string - Year string - ReservationTimes []api.Time + ReservationTimes []time.Time PartySize int - RequestDay string - RequestMonth string - RequestYear string - RequestTime api.Time + RequestTime time.Time } /* @@ -105,8 +97,8 @@ Type: interface Purpose: Provide a common definition for an operation result */ -type Timeable interface { - Time() (api.Time) +type Timetable interface { + Time() (time.Time) } /* @@ -116,15 +108,15 @@ Purpose: Define the data that should be returned on a successful reserve at interval response */ type ReserveAtIntervalResponse struct { - ReservationTime api.Time + ReservationTime time.Time } /* Name: Time Type: interface method -Purpose: Satisfy the Timeable interface +Purpose: Satisfy the Timetable interface */ -func (r ReserveAtIntervalResponse) Time() (api.Time) { +func (r ReserveAtIntervalResponse) Time() (time.Time) { return r.ReservationTime } @@ -135,15 +127,15 @@ Purpose: Define the data that should be returned on a successful reserve at time response */ type ReserveAtTimeResponse struct { - ReservationTime api.Time + ReservationTime time.Time } /* Name: Time Type: interface method -Purpose: Satisfy the Timeable interface +Purpose: Satisfy the Timetable interface */ -func (r ReserveAtTimeResponse) Time() (api.Time) { +func (r ReserveAtTimeResponse) Time() (time.Time) { return r.ReservationTime } @@ -154,7 +146,7 @@ Purpose: Define a consistent way of conveying a successful operation */ type OperationResult struct { - Response Timeable + Response Timetable Err error } @@ -172,113 +164,25 @@ type Operation struct{ Status OperationStatus } + /* Name: findLastTime Type: Internal Func Purpose: Out of a list of times, return the latest time */ -func findLastTime(times []api.Time) (*api.Time, error) { +func findLastTime(times []time.Time) (*time.Time, error) { if len(times) == 0 { return nil, api.ErrTimeNull } lastTime := times[0] for i := 1; i < len(times); i++{ - lstHr, err := strconv.ParseInt(lastTime.Hour, 10, 64) - if err != nil { - return nil, err - } - lstMn, err := strconv.ParseInt(lastTime.Minute, 10, 64) - if err != nil { - return nil, err - } - thsHr, err := strconv.ParseInt(times[i].Hour, 10, 64) - if err != nil { - return nil, err - } - thsMn, err := strconv.ParseInt(times[i].Minute, 10, 64) - if err != nil { - return nil, err + if times[i].After(lastTime) { + lastTime = times[i] } - if (lstHr < thsHr) || (lstHr == thsHr && lstMn < thsMn) { - lastTime = times[i] - } } return &lastTime, nil } -/* -Name: dateStringsToInts -Type: Internal Func -Purpose: Convert the input string list to ints, -presumed to be in date string format for this -function -*/ -func dateStringsToInts(in []string) ([]int, error) { - out := make([]int, len(in), len(in)) - for i, s := range in { - raw, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return nil, err - } - out[i] = int(raw) - } - return out, nil -} - -/* -Name: isTimeUTCFuture -Type: Internal Func -Purpose: Test if the input integer times, -interpreted as UTC respective time -are in the future -*/ -func isTimeUTCFuture(year, month, day, hour, minute int) bool{ - now := time.Now().UTC() - nowYear, nowMonth, nowDay := now.Date() - yrCmp := nowYear < year - yrEq := nowYear == year - mtCmp := int(nowMonth) < month - mtEq := int(nowMonth) == month - dyCmp := nowDay < day - dyEq := nowDay == day - hrCmp := now.Hour() < hour - hrEq := now.Hour() == hour - mnCmp := now.Minute() < minute - cmp := yrCmp || - (yrEq && mtCmp) || - (yrEq && mtEq && dyCmp) || - (yrEq && mtEq && dyEq && hrCmp) || - (yrEq && mtEq && dyEq && hrEq && mnCmp) - return cmp -} - -/* -Name: isTimeLocalFuture -Type: Internal Func -Purpose: Test if the input integer times, -interpreted as Local respective time -are in the future -*/ -func isTimeLocalFuture(year, month, day, hour, minute int) bool{ - now := time.Now() - nowYear, nowMonth, nowDay := now.Date() - yrCmp := nowYear < year - yrEq := nowYear == year - mtCmp := int(nowMonth) < month - mtEq := int(nowMonth) == month - dyCmp := nowDay < day - dyEq := nowDay == day - hrCmp := now.Hour() < hour - hrEq := now.Hour() == hour - mnCmp := now.Minute() < minute - cmp := yrCmp || - (yrEq && mtCmp) || - (yrEq && mtEq && dyCmp) || - (yrEq && mtEq && dyEq && hrCmp) || - (yrEq && mtEq && dyEq && hrEq && mnCmp) - return cmp -} - /* Name: updateOperationResult Type: Internal Func @@ -390,22 +294,6 @@ a reservation at a given interval of time func (a *AppCtx) reserveAtInterval(params ReserveAtIntervalParam, cancel <-chan bool, output chan<- OperationResult){ // find and store last time from time priority list lastTime, err := findLastTime(params.ReservationTimes) - if err != nil { - output<-OperationResult{Response: nil, Err: err} - close(output) - return - } - - // convert time strings to integers - dateInts, err := dateStringsToInts([]string{ - params.RepeatInterval.Hour, - params.RepeatInterval.Minute, - params.Year, - params.Month, - params.Day, - lastTime.Hour, - lastTime.Minute, - }) if err != nil { output<-OperationResult{Response: nil, Err: err} @@ -413,17 +301,6 @@ func (a *AppCtx) reserveAtInterval(params ReserveAtIntervalParam, cancel <-chan return } - numHrs := dateInts[0] - numMns := dateInts[1] - year := dateInts[2] - month := dateInts[3] - day := dateInts[4] - hour := dateInts[5] - minute := dateInts[6] - - // convert interval to a 'time.Duration', which can be used in go time.After() - repeatInterval := time.Hour * time.Duration(numHrs) + time.Minute * time.Duration(numMns) - for { // first run pre reservation auth @@ -439,9 +316,6 @@ func (a *AppCtx) reserveAtInterval(params ReserveAtIntervalParam, cancel <-chan reserveResp, err := a.API.Reserve( api.ReserveParam{ LoginResp: *loginResp, - Day: params.Day, - Month: params.Month, - Year: params.Year, ReservationTimes: params.ReservationTimes, PartySize: params.PartySize, VenueID: params.VenueID, @@ -457,10 +331,9 @@ func (a *AppCtx) reserveAtInterval(params ReserveAtIntervalParam, cancel <-chan if err == api.ErrNoTable { // see if last time on list is still in the future, // since if it isn't there's no point in trying to reserve it - cmp := isTimeLocalFuture(year, month, day, hour, minute) - if cmp { + if lastTime.After(time.Now()) { select { - case <-time.After(repeatInterval): + case <-time.After(params.RepeatInterval): continue case <-cancel: output<-OperationResult{Response: nil, Err: ErrCancel} @@ -518,48 +391,40 @@ Purpose: This function is intended to run on a separate thread, and tries making a reservation at a given time */ func (a *AppCtx) reserveAtTime(params ReserveAtTimeParam, cancel <-chan bool, output chan<- OperationResult) { - // convert date strings to ints - dateInts, err := dateStringsToInts([]string{ - params.RequestTime.Hour, - params.RequestTime.Minute, - params.RequestYear, - params.RequestMonth, - params.RequestDay, - }) - if err != nil { - output <- OperationResult{Response: nil, Err:err} - close(output) - return - } - // set date strings to locals - hour := dateInts[0] - minute := dateInts[1] - year := dateInts[2] - month := dateInts[3] - day := dateInts[4] - + // if this date is not in the future, err - if !isTimeUTCFuture(year, month, day, hour, minute) { + if params.RequestTime.Before(time.Now().UTC()) { output <- OperationResult{Response: nil, Err: ErrTimeFut} close(output) return } - requestTime := time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.UTC) - // sleep with ability to cancel - select { - case <-time.After(time.Until(requestTime)): - case <-cancel: - output<- OperationResult{Response: nil, Err:ErrCancel} - close(output) - return + minAuthTime := a.API.AuthMinExpire() + authDate := params.RequestTime.Add(-1 * minAuthTime) + if (!authDate.Before(time.Now().UTC())) { + select { + case <-time.After(time.Until(authDate)): + break + case <-cancel: + output<- OperationResult{Response: nil, Err:ErrCancel} + close(output) + return + } } - // attempt pre reserve auth loginResp, err := a.API.Login(api.LoginParam(params.Login)) - + if err != nil { - output<- OperationResult{Response: nil, Err:err} + output<- OperationResult{Response: nil, Err:err} + close(output) + return + } + + // sleep with ability to cancel + select { + case <-time.After(time.Until(params.RequestTime)): + case <-cancel: + output<- OperationResult{Response: nil, Err:ErrCancel} close(output) return } @@ -568,9 +433,6 @@ func (a *AppCtx) reserveAtTime(params ReserveAtTimeParam, cancel <-chan bool, ou reserveResp, err := a.API.Reserve( api.ReserveParam{ LoginResp: *loginResp, - Day: params.Day, - Month: params.Month, - Year: params.Year, ReservationTimes: params.ReservationTimes, PartySize: params.PartySize, VenueID: params.VenueID, @@ -690,7 +552,7 @@ func (a *AppCtx) OperationsToString() (string, error) { case SuccessStatusType: time := operation.Result.Response.Time() opLstStr += "Succeeded\n" - opLstStr += "\tResult: " + time.Hour + ":" + time.Minute + opLstStr += fmt.Sprintf("\tResult: %02d:%02d", time.Hour(), time.Minute()) case FailStatusType: err := operation.Result.Err.Error() opLstStr += "Failed\n" diff --git a/runnable/cli/runnable.go b/runnable/cli/runnable.go index 102a1f3..13facfe 100644 --- a/runnable/cli/runnable.go +++ b/runnable/cli/runnable.go @@ -173,17 +173,36 @@ func (c *ResolvedCLI) parseRats(in map[string][]string) (*app.ReserveAtTimeParam if len(resDaySplt) != 3 { return nil, ErrInvDate } - req.Year = resDaySplt[0] - req.Month = resDaySplt[1] - req.Day = resDaySplt[2] - req.ReservationTimes = make([]api.Time, len(in["resT"]), len(in["resT"])) + + reqYear, err := strconv.Atoi(resDaySplt[0]) + if err != nil { + return nil, err + } + reqMonth, err := strconv.Atoi(resDaySplt[1]) + if err != nil { + return nil, err + } + reqDay, err := strconv.Atoi(resDaySplt[2]) + if err != nil { + return nil, err + } + + req.ReservationTimes = make([]time.Time, len(in["resT"]), len(in["resT"])) for i, timeStr := range in["resT"] { timeSplt := strings.Split(timeStr, ":") if len(timeSplt) != 2 { return nil, ErrInvDate } - req.ReservationTimes[i].Hour = timeSplt[0] - req.ReservationTimes[i].Minute = timeSplt[1] + reqHour, err := strconv.Atoi(timeSplt[0]) + if err != nil { + return nil, err + } + reqMin, err := strconv.Atoi(timeSplt[1]) + if err != nil { + return nil, err + } + req.ReservationTimes[i] = time.Date(reqYear, time.Month(reqMonth), reqDay, reqHour, reqMin, 0, 0, time.Local) + } ps, err := strconv.ParseInt(in["ps"][0], 10, 64) if err != nil { @@ -219,12 +238,7 @@ func (c *ResolvedCLI) parseRats(in map[string][]string) (*app.ReserveAtTimeParam } timeLoc := time.Date(int(year), time.Month(int(month)), int(day), int(hour), int(minute), 0, 0, time.Local) timeUTC := timeLoc.UTC() - yearu, monthu, dayu := timeUTC.Date() - req.RequestYear = strconv.Itoa(yearu) - req.RequestMonth = strconv.Itoa(int(monthu)) - req.RequestDay = strconv.Itoa(dayu) - req.RequestTime.Hour = strconv.Itoa(timeUTC.Hour()) - req.RequestTime.Minute = strconv.Itoa(timeUTC.Minute()) + req.RequestTime = timeUTC return &req, nil } @@ -278,17 +292,33 @@ func (c *ResolvedCLI) parseRais(in map[string][]string) (*app.ReserveAtIntervalP if len(resDaySplt) != 3 { return nil, ErrInvDate } - req.Year = resDaySplt[0] - req.Month = resDaySplt[1] - req.Day = resDaySplt[2] - req.ReservationTimes = make([]api.Time, len(in["resT"]), len(in["resT"])) + reqYear, err := strconv.Atoi(resDaySplt[0]) + if err != nil { + return nil, err + } + reqMonth, err := strconv.Atoi(resDaySplt[1]) + if err != nil { + return nil, err + } + reqDay, err := strconv.Atoi(resDaySplt[2]) + if err != nil { + return nil, err + } + req.ReservationTimes = make([]time.Time, len(in["resT"]), len(in["resT"])) for i, timeStr := range in["resT"] { timeSplt := strings.Split(timeStr, ":") if len(timeSplt) != 2 { return nil, ErrInvDate } - req.ReservationTimes[i].Hour = timeSplt[0] - req.ReservationTimes[i].Minute = timeSplt[1] + reqHour, err := strconv.Atoi(timeSplt[0]) + if err != nil { + return nil, err + } + reqMin, err := strconv.Atoi(timeSplt[1]) + if err != nil { + return nil, err + } + req.ReservationTimes[i] = time.Date(reqYear, time.Month(reqMonth), reqDay, reqHour, reqMin, 0, 0, time.Local) } ps, err := strconv.ParseInt(in["ps"][0], 10, 64) if err != nil { @@ -301,9 +331,17 @@ func (c *ResolvedCLI) parseRais(in map[string][]string) (*app.ReserveAtIntervalP if len(repIntSplt) != 2 { return nil, ErrInvDate } - req.RepeatInterval.Hour = repIntSplt[0] - req.RepeatInterval.Minute = repIntSplt[1] - + + repHour, err := strconv.Atoi(repIntSplt[0]) + if err != nil { + return nil, err + } + repMin, err := strconv.Atoi(repIntSplt[1]) + if err != nil { + return nil, err + } + req.RepeatInterval = time.Hour * time.Duration(repHour) + time.Minute * time.Duration(repMin) + return &req, nil }