From db0194e6f720c0c7e3e4b614285fed5605035907 Mon Sep 17 00:00:00 2001 From: Emmanuel Odeke Date: Tue, 18 Jul 2017 18:07:10 -0600 Subject: [PATCH] cli: implement history, init, order Fixes #44 Implements CLI with multiple commands: * history * init * order Firstly to install the CLI: ```shell $ go get -u -v github.com/orijtech/uber/cmd/uber ``` Exhibits: * history: List your last 3 trips ```shell $ uber history --limit-per-page 3 --max-page 1 Page: #1 +--------+---------------+-------------------------+----------+-------+--------------------------------------+ | TRIP # | CITY | DATE | DURATION | MILES | REQUESTID | +--------+---------------+-------------------------+----------+-------+--------------------------------------+ | 1 | Denver | 2017/07/15 21:47:44 MDT | 7m31s | 3.211 | 8e7f479c-63e2-4ccc-babd-8671771485c3 | +--------+---------------+-------------------------+----------+-------+--------------------------------------+ | 2 | San Francisco | 2017/07/13 18:11:06 MDT | 14m16s | 3.694 | d521aed9-e9bc-4673-9109-25d9ce5c434c | +--------+---------------+-------------------------+----------+-------+--------------------------------------+ | 3 | London | 2017/06/25 16:17:43 MDT | 13m35s | 3.318 | 1ce3cccb-2e09-4920-ad80-d00a4645f9ce | +--------+---------------+-------------------------+----------+-------+--------------------------------------+ ``` * init init initializes the context and authorization for your Uber app in the current working directory ```shell $ go get -u -v github.com/orijtech/uber/cmd/uber $ uber init Please visit this URL for the auth dialog: https://login.uber.com/oauth/v2/authorize?access_type=offline&client_id=a_client_id&redirect_uri=https%3A%2F%2Fexample.org/uber&response_type=code&scope=profile+request+history+places+request_receipt+delivery&state=15004223370.604660 ``` which after successful authorization will give you a notice in your browser, to return to your terminal and will save the token to a file on disk, for example: ```shell Successfully saved your OAuth2.0 token to "/Users/orijtech/uber-account/.uber/credentials.json" ``` * order ```shell $ uber order Start Point: Redwood City Cinemark +--------+--------------------------------+-----------+-----------+-------------+ | CHOICE | NAME | RELEVANCE | LATITUDE | LONGITUDE | +--------+--------------------------------+-----------+-----------+-------------+ | 0 | Cinemark 20 Redwood City, | 98.70% | 37.485912 | -122.228752 | | | 825 Middlefield Rd, Redwood | | | | | | City, California 94063, United | | | | | | States | | | | +--------+--------------------------------+-----------+-----------+-------------+ | 1 | Redwood City, California, | 49.00% | 37.485199 | -122.236397 | | | United States | | | | +--------+--------------------------------+-----------+-----------+-------------+ | 2 | Redwood City Station, 805 | 39.00% | 37.485439 | -122.231796 | | | Veterans Blvd, Redwood City, | | | | | | California 94063, United | | | | | | States | | | | +--------+--------------------------------+-----------+-----------+-------------+ | 3 | Cinemark Ave, Markham, Ontario | 39.00% | 43.887989 | -79.225441 | | | L6B 1E3, Canada | | | | +--------+--------------------------------+-----------+-----------+-------------+ | 4 | Cinemark Ct, Mulberry, Florida | 39.00% | 27.934687 | -81.996933 | | | 33860, United States | | | | +--------+--------------------------------+-----------+-----------+-------------+ Please enter your choice by numeric key or (n) to search again: 0 End Point: Palo Alto +--------+--------------------------------+-----------+-----------+-------------+ | CHOICE | NAME | RELEVANCE | LATITUDE | LONGITUDE | +--------+--------------------------------+-----------+-----------+-------------+ | 0 | Palo Alto, California, United | 99.00% | 37.442200 | -122.163399 | | | States | | | | +--------+--------------------------------+-----------+-----------+-------------+ | 1 | Palo Alto Battlefield National | 99.00% | 26.021400 | -97.480598 | | | Historical Park, 7200 PAREDES | | | | | | LINE Rd, Los Fresnos, Texas | | | | | | 78566, United States | | | | +--------+--------------------------------+-----------+-----------+-------------+ | 2 | Palo Alto Baylands Nature | 99.00% | 37.459599 | -122.106003 | | | Preserve, 2500 Embarcadero | | | | | | Way, East Palo Alto, | | | | | | California 94303, United | | | | | | States | | | | +--------+--------------------------------+-----------+-----------+-------------+ | 3 | Palo Alto University, 1791 | 99.00% | 37.382301 | -122.188004 | | | Arastradero Rd, Palo Alto, | | | | | | California 94304, United | | | | | | States | | | | +--------+--------------------------------+-----------+-----------+-------------+ | 4 | Palo Alto High School, 50 | 99.00% | 37.437000 | -122.156998 | | | Embarcadero Rd, Palo Alto, | | | | | | California 94306, United | | | | | | States | | | | +--------+--------------------------------+-----------+-----------+-------------+ Please enter your choice by numeric key or (n) to search again: 0 Seat count: 1 or 2 (default 2) 1 +--------+--------+----------+----------+----------------------+--------------------+ | CHOICE | NAME | ESTIMATE | CURRENCY | PICKUP ETA (MINUTES) | DURATION (MINUTES) | +--------+--------+----------+----------+----------------------+--------------------+ | 0 | SELECT | $31-39 | USD | 3.0 | 22.0 | +--------+--------+----------+----------+----------------------+--------------------+ | 1 | ASSIST | $15-19 | USD | 10.0 | 22.0 | +--------+--------+----------+----------+----------------------+--------------------+ | 2 | uberXL | $19-24 | USD | 12.0 | 22.0 | +--------+--------+----------+----------+----------------------+--------------------+ | 3 | BLACK | $40-50 | USD | 5.0 | 22.0 | +--------+--------+----------+----------+----------------------+--------------------+ | 4 | SUV | $53-65 | USD | 5.0 | 22.0 | +--------+--------+----------+----------+----------------------+--------------------+ | 5 | WAV | $13-16 | USD | 0.0 | 22.0 | +--------+--------+----------+----------+----------------------+--------------------+ | 6 | POOL | $6-8 | USD | 9.0 | 22.0 | +--------+--------+----------+----------+----------------------+--------------------+ | 7 | uberX | $15-19 | USD | 8.0 | 22.0 | +--------+--------+----------+----------+----------------------+--------------------+ Please enter the choice of your item or n to cancel ``` --- .gitignore | 3 + README.md | 144 +++++++++++++- cmd/uber/Makefile | 35 ++++ cmd/uber/main.go | 482 ++++++++++++++++++++++++++++------------------ 4 files changed, 471 insertions(+), 193 deletions(-) create mode 100644 cmd/uber/Makefile diff --git a/.gitignore b/.gitignore index f4a9135..00d13b4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ _testmain.go # Text editor and miscellaneous files *.sw[op] *.DS_Store + +# Binary directories +bin/ diff --git a/README.md b/README.md index a2032fd..d37f404 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,19 @@ # uber Uber API client in Go -* Requirement: +## Table of contents +- [Requirements](#requirements) +- [CLI](#cli) + - [Installation](#installation) + - [init](#init) + - [history](#history) + - [order](#order) +- [SDK Usage](#sdk-usage) + +## Requirements: To use client v1, you'll need to set + `UBER_TOKEN_KEY` -## CLI installation -```go -$ go get -u -v github.com/orijtech/uber/cmd/uber -$ uber --order -``` - ## SDK usage Sample usage: You can see file [example_test.go](./example_test.go) @@ -420,3 +423,130 @@ func cancelDelivery() { } } ``` + +## CLI +### Installation +```go +$ go get -u -v github.com/orijtech/uber/cmd/uber +``` + +### init +init initializes the context and authorization for your Uber app in the current working directory + +```shell +$ go get -u -v github.com/orijtech/uber/cmd/uber +$ uber init +Please visit this URL for the auth dialog: https://login.uber.com/oauth/v2/authorize?access_type=offline&client_id=a_client_id&redirect_uri=https%3A%2F%2Fexample.org/uber&response_type=code&scope=profile+request+history+places+request_receipt+delivery&state=15004223370.604660 +``` +which after successful authorization will give you a notice in your browser, to return to +your terminal and will save the token to a file on disk, for example: +```shell +Successfully saved your OAuth2.0 token to "/Users/orijtech/uber-account/.uber/credentials.json" +``` + +From then on, for that Uber account, please go into that directory "/Users/orijtech/uber-account/" +in order to use that account + +### history +history allows you to retrieve and examine your previous trips in a tabular form + +```shell +$ uber history -h +``` +for all available options. + +* List your last 3 trips +```shell +$ uber history --limit-per-page 3 --max-page 1 + +Page: #1 ++--------+---------------+-------------------------+----------+-------+--------------------------------------+ +| TRIP # | CITY | DATE | DURATION | MILES | REQUESTID | ++--------+---------------+-------------------------+----------+-------+--------------------------------------+ +| 1 | Denver | 2017/07/15 21:47:44 MDT | 7m31s | 3.211 | 8e7f479c-63e2-4ccc-babd-8671771485c3 | ++--------+---------------+-------------------------+----------+-------+--------------------------------------+ +| 2 | San Francisco | 2017/07/13 18:11:06 MDT | 14m16s | 3.694 | d521aed9-e9bc-4673-9109-25d9ce5c434c | ++--------+---------------+-------------------------+----------+-------+--------------------------------------+ +| 3 | London | 2017/06/25 16:17:43 MDT | 13m35s | 3.318 | 1ce3cccb-2e09-4920-ad80-d00a4645f9ce | ++--------+---------------+-------------------------+----------+-------+--------------------------------------+ +``` + +### order +order allows you to order an Uber to any location and destination +```shell +$ uber order +Start Point: Redwood City Cinemark ++--------+--------------------------------+-----------+-----------+-------------+ +| CHOICE | NAME | RELEVANCE | LATITUDE | LONGITUDE | ++--------+--------------------------------+-----------+-----------+-------------+ +| 0 | Cinemark 20 Redwood City, | 98.70% | 37.485912 | -122.228752 | +| | 825 Middlefield Rd, Redwood | | | | +| | City, California 94063, United | | | | +| | States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +| 1 | Redwood City, California, | 49.00% | 37.485199 | -122.236397 | +| | United States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +| 2 | Redwood City Station, 805 | 39.00% | 37.485439 | -122.231796 | +| | Veterans Blvd, Redwood City, | | | | +| | California 94063, United | | | | +| | States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +| 3 | Cinemark Ave, Markham, Ontario | 39.00% | 43.887989 | -79.225441 | +| | L6B 1E3, Canada | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +| 4 | Cinemark Ct, Mulberry, Florida | 39.00% | 27.934687 | -81.996933 | +| | 33860, United States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +Please enter your choice by numeric key or (n) to search again: 0 +End Point: Palo Alto ++--------+--------------------------------+-----------+-----------+-------------+ +| CHOICE | NAME | RELEVANCE | LATITUDE | LONGITUDE | ++--------+--------------------------------+-----------+-----------+-------------+ +| 0 | Palo Alto, California, United | 99.00% | 37.442200 | -122.163399 | +| | States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +| 1 | Palo Alto Battlefield National | 99.00% | 26.021400 | -97.480598 | +| | Historical Park, 7200 PAREDES | | | | +| | LINE Rd, Los Fresnos, Texas | | | | +| | 78566, United States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +| 2 | Palo Alto Baylands Nature | 99.00% | 37.459599 | -122.106003 | +| | Preserve, 2500 Embarcadero | | | | +| | Way, East Palo Alto, | | | | +| | California 94303, United | | | | +| | States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +| 3 | Palo Alto University, 1791 | 99.00% | 37.382301 | -122.188004 | +| | Arastradero Rd, Palo Alto, | | | | +| | California 94304, United | | | | +| | States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +| 4 | Palo Alto High School, 50 | 99.00% | 37.437000 | -122.156998 | +| | Embarcadero Rd, Palo Alto, | | | | +| | California 94306, United | | | | +| | States | | | | ++--------+--------------------------------+-----------+-----------+-------------+ +Please enter your choice by numeric key or (n) to search again: 0 +Seat count: 1 or 2 (default 2) 1 ++--------+--------+----------+----------+----------------------+--------------------+ +| CHOICE | NAME | ESTIMATE | CURRENCY | PICKUP ETA (MINUTES) | DURATION (MINUTES) | ++--------+--------+----------+----------+----------------------+--------------------+ +| 0 | SELECT | $31-39 | USD | 3.0 | 22.0 | ++--------+--------+----------+----------+----------------------+--------------------+ +| 1 | ASSIST | $15-19 | USD | 10.0 | 22.0 | ++--------+--------+----------+----------+----------------------+--------------------+ +| 2 | uberXL | $19-24 | USD | 12.0 | 22.0 | ++--------+--------+----------+----------+----------------------+--------------------+ +| 3 | BLACK | $40-50 | USD | 5.0 | 22.0 | ++--------+--------+----------+----------+----------------------+--------------------+ +| 4 | SUV | $53-65 | USD | 5.0 | 22.0 | ++--------+--------+----------+----------+----------------------+--------------------+ +| 5 | WAV | $13-16 | USD | 0.0 | 22.0 | ++--------+--------+----------+----------+----------------------+--------------------+ +| 6 | POOL | $6-8 | USD | 9.0 | 22.0 | ++--------+--------+----------+----------+----------------------+--------------------+ +| 7 | uberX | $15-19 | USD | 8.0 | 22.0 | ++--------+--------+----------+----------+----------------------+--------------------+ +Please enter the choice of your item or n to cancel +``` diff --git a/cmd/uber/Makefile b/cmd/uber/Makefile new file mode 100644 index 0000000..192d198 --- /dev/null +++ b/cmd/uber/Makefile @@ -0,0 +1,35 @@ +# Makefile for cross-compilation +# +OS := $(shell uname) +BINDIR := ./bin +MD5_TEXTFILE := $(BINDIR)/md5Sums.txt + +MAIN_FILE_DIR := ./ + +ifeq ($(OS), Darwin) + MD5_UTIL = md5 +else + MD5_UTIL = md5sum +endif + +all: compileThemAll md5SumThemAll + +compileThemAll: armv5 armv6 armv7 armv8 darwin linux + +md5SumThemAll: + rm -f $(MD5_TEXTFILE) + find $(BINDIR) -type f -name "uber_*" -exec $(MD5_UTIL) {} >> $(MD5_TEXTFILE) \; + cat $(MD5_TEXTFILE) + +armv5: + CGO_ENABLED=0 GOOS=linux GOARM=5 GOARCH=arm go build -o $(BINDIR)/uber_armv5 $(MAIN_FILE_DIR) +armv6: + CGO_ENABLED=0 GOOS=linux GOARM=6 GOARCH=arm go build -o $(BINDIR)/uber_armv6 $(MAIN_FILE_DIR) +armv7: + CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -o $(BINDIR)/uber_armv7 $(MAIN_FILE_DIR) +armv8: + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $(BINDIR)/uber_armv8 $(MAIN_FILE_DIR) +darwin: + CGO_ENABLED=0 GOOS=darwin go build -o $(BINDIR)/uber_darwin $(MAIN_FILE_DIR) +linux: + CGO_ENABLED=0 GOOS=linux go build -o $(BINDIR)/uber_linux $(MAIN_FILE_DIR) diff --git a/cmd/uber/main.go b/cmd/uber/main.go index 0277653..eac467d 100644 --- a/cmd/uber/main.go +++ b/cmd/uber/main.go @@ -24,6 +24,7 @@ import ( "path/filepath" "strconv" "strings" + "time" "github.com/orijtech/uber/oauth2" "github.com/orijtech/uber/v1" @@ -31,6 +32,7 @@ import ( "github.com/olekukonko/tablewriter" "github.com/odeke-em/cli-spinner" + "github.com/odeke-em/command" "github.com/odeke-em/go-utils/fread" "github.com/odeke-em/mapbox" "github.com/odeke-em/semalim" @@ -40,207 +42,347 @@ const repeatSentinel = "n" var mapboxClient *mapbox.Client -func init() { - var err error - mapboxClient, err = mapbox.NewClient() +type initCmd struct { +} + +var _ command.Cmd = (*initCmd)(nil) + +func (a *initCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { + return fs +} + +func exitIfErr(err error) { if err != nil { - log.Fatal(err) + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(-1) } } -func main() { - log.SetFlags(0) +const ( + uberCredsDir = ".uber" + credentialsOutFile = "credentials.json" +) - uberClient, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) +func (a *initCmd) Run(args []string, defaults map[string]*flag.Flag) { + uberCredsDirPath, err := ensureUberCredsDirExists() + if err != nil { + exitIfErr(fmt.Errorf("init: os.MkdirAll(%q) err=%q", err)) + } + + scopes := []string{ + oauth2.ScopeProfile, oauth2.ScopeRequest, + oauth2.ScopeHistory, oauth2.ScopePlaces, + oauth2.ScopeRequestReceipt, oauth2.ScopeDelivery, + } + + token, err := oauth2.AuthorizeByEnvApp(scopes...) if err != nil { log.Fatal(err) } - var init bool - var order bool - flag.BoolVar(&init, "init", false, "allow a user to authorize this app to make requests on their behalf") - flag.BoolVar(&order, "order", false, "order an Uber") - flag.Parse() + blob, err := json.Marshal(token) + if err != nil { + log.Fatal(err) + } - // Make log not print out time info in its prefix. - log.SetFlags(0) + credsPath := filepath.Join(uberCredsDirPath, credentialsOutFile) + f, err := os.Create(credsPath) + if err != nil { + log.Fatal(err) + } - switch { - case init: - authorize() - case order: - spinr := spinner.New(10) - var startGeocodeFeature, endGeocodeFeature mapbox.GeocodeFeature - items := [...]struct { - ft *mapbox.GeocodeFeature - prompt string - }{ - 0: {&startGeocodeFeature, "Start Point: "}, - 1: {&endGeocodeFeature, "End Point: "}, - } + f.Write(blob) + log.Printf("Successfully saved your OAuth2.0 token to %q", credsPath) +} - linesChan := fread.Fread(os.Stdin) - for i, item := range items { - for { - geocodeFeature, query, err := doSearch(item.prompt, linesChan, "n", spinr) - if err == nil { - *item.ft = *geocodeFeature - break - } +func uberClientFromFile(path string) (*uber.Client, error) { + return uber.NewClientFromOAuth2File(path) +} - switch err { - case errRepeat: - fmt.Printf("\033[32mSearching again *\033[00m\n") - continue - case errNoMatchFound: - fmt.Printf("No matches found found for %q. Try again? (y/N) ", query) - continueResponse := strings.TrimSpace(<-linesChan) - if strings.HasPrefix(strings.ToLower(continueResponse), "y") { - continue - } - return - default: - // Otherwise an unhandled error - log.Fatalf("%d: search err: %v; prompt=%q", i, err, item.prompt) - } - } - } +type historyCmd struct { + maxPage int + limitPerPage int + noPrompt bool + pageOffset int + throttleStr string +} - var seatCount int = 2 - for { - fmt.Printf("Seat count: 1 or 2 (default 2) ") - seatCountLine := strings.TrimSpace(<-linesChan) - if seatCountLine == "" { - seatCount = 2 - break - } else { - parsed, err := strconv.ParseInt(seatCountLine, 10, 32) - if err != nil { - log.Fatalf("seatCount parsing err: %v", err) - } - if parsed >= 1 && parsed <= 2 { - seatCount = int(parsed) - break - } else { - fmt.Printf("\033[31mPlease enter either 1 or 2!\033[00m\n") - } - } - } +var _ command.Cmd = (*historyCmd)(nil) - startCoord := centerToCoord(startGeocodeFeature.Center) - endCoord := centerToCoord(endGeocodeFeature.Center) - esReq := &uber.EstimateRequest{ - StartLatitude: startCoord.Lat, - StartLongitude: startCoord.Lng, - EndLatitude: endCoord.Lat, - EndLongitude: endCoord.Lng, - SeatCount: seatCount, - } +func (h *historyCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { + fs.IntVar(&h.maxPage, "max-page", 4, "the maximum number of pages to show") + fs.BoolVar(&h.noPrompt, "no-prompt", false, "if set, do not prompt") + fs.IntVar(&h.limitPerPage, "limit-per-page", 0, "limits the number of items retrieved per page") + fs.IntVar(&h.pageOffset, "page-offset", 0, "positions where to start pagination from") + fs.StringVar(&h.throttleStr, "throttle", "", "the throttle duration e.g 8s, 10m, 7ms as per https://golang.org/pkg/time/#ParseDuration") + return fs +} - estimates, err := doUberEstimates(uberClient, esReq, spinr) - if err != nil { - log.Fatalf("estimate err: %v\n", err) +func credsMustExist() string { + wdir, err := os.Getwd() + if err != nil { + exitIfErr(fmt.Errorf("credentials: os.Getwd err=%q", err)) + } + + fullPath := filepath.Join(wdir, uberCredsDir, credentialsOutFile) + _, err = os.Stat(fullPath) + exitIfErr(err) + + return fullPath +} + +func (h *historyCmd) Run(args []string, defaults map[string]*flag.Flag) { + credsPath := credsMustExist() + client, err := uberClientFromFile(credsPath) + exitIfErr(err) + + // If an invalid duration is passed, it'll return + // the zero value which is the same as passing in nothing. + throttle, _ := time.ParseDuration(h.throttleStr) + + pagesChan, _, err := client.ListHistory(&uber.Pager{ + LimitPerPage: int64(h.limitPerPage), + MaxPages: int64(h.maxPage), + StartOffset: int64(h.pageOffset), + + ThrottleDuration: throttle, + }) + + exitIfErr(err) + + for page := range pagesChan { + if page.Err != nil { + fmt.Printf("Page: #%d err: %v\n", page.PageNumber, page.Err) + continue } table := tablewriter.NewWriter(os.Stdout) table.SetRowLine(true) table.SetHeader([]string{ - "Choice", "Name", "Estimate", "Currency", - "Pickup time (minutes)", "TripDuration (minutes)", + "Trip #", "City", "Date", + "Duration", "Miles", "RequestID", }) - for i, est := range estimates { - et := est.Estimate - ufare := est.UpfrontFare + fmt.Printf("\n\033[32mPage: #%d\033[00m\n", page.PageNumber+1) + for i, trip := range page.Trips { + startCity := trip.StartCity + startDate := time.Unix(trip.StartTimeUnix, 0) + endDate := time.Unix(trip.EndTimeUnix, 0) table.Append([]string{ - fmt.Sprintf("%d", i), - fmt.Sprintf("%s", et.LocalizedName), - fmt.Sprintf("%s", et.Estimate), - fmt.Sprintf("%s", et.CurrencyCode), - fmt.Sprintf("%.1f", ufare.PickupEstimateMinutes), - fmt.Sprintf("%.1f", et.DurationSeconds/60.0), + fmt.Sprintf("%d", i+1), + fmt.Sprintf("%s", startCity.Name), + fmt.Sprintf("%s", startDate.Format("2006/01/02 15:04:05 MST")), + fmt.Sprintf("%s", endDate.Sub(startDate)), + fmt.Sprintf("%.3f", trip.DistanceMiles), + fmt.Sprintf("%s", trip.RequestID), }) } table.Render() + } +} + +type orderCmd struct { +} + +var _ command.Cmd = (*orderCmd)(nil) + +func (o *orderCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { + return fs +} - var estimateChoice *estimateAndUpfrontFarePair +func (o *orderCmd) Run(args []string, defaults map[string]*flag.Flag) { + credsPath := credsMustExist() + uberClient, err := uberClientFromFile(credsPath) + exitIfErr(err) + + spinr := spinner.New(10) + var startGeocodeFeature, endGeocodeFeature mapbox.GeocodeFeature + items := [...]struct { + ft *mapbox.GeocodeFeature + prompt string + }{ + 0: {&startGeocodeFeature, "Start Point: "}, + 1: {&endGeocodeFeature, "End Point: "}, + } + linesChan := fread.Fread(os.Stdin) + for i, item := range items { for { - fmt.Printf("Please enter the choice of your item or n to cancel ") + geocodeFeature, query, err := doSearch(item.prompt, linesChan, "n", spinr) + if err == nil { + *item.ft = *geocodeFeature + break + } - lineIn := strings.TrimSpace(<-linesChan) - if strings.EqualFold(lineIn, repeatSentinel) { + switch err { + case errRepeat: + fmt.Printf("\033[32mSearching again *\033[00m\n") + continue + case errNoMatchFound: + fmt.Printf("No matches found found for %q. Try again? (y/N) ", query) + continueResponse := strings.TrimSpace(<-linesChan) + if strings.HasPrefix(strings.ToLower(continueResponse), "y") { + continue + } return + default: + // Otherwise an unhandled error + log.Fatalf("%d: search err: %v; prompt=%q", i, err, item.prompt) } + } + } - choice, err := strconv.ParseUint(lineIn, 10, 32) + var seatCount int = 2 + for { + fmt.Printf("Seat count: 1 or 2 (default 2) ") + seatCountLine := strings.TrimSpace(<-linesChan) + if seatCountLine == "" { + seatCount = 2 + break + } else { + parsed, err := strconv.ParseInt(seatCountLine, 10, 32) if err != nil { - log.Fatalf("parsing choice err: %v", err) + log.Fatalf("seatCount parsing err: %v", err) } - if choice < 0 || choice >= uint64(len(estimates)) { - log.Fatalf("choice must be >=0 && < %d", len(estimates)) + if parsed >= 1 && parsed <= 2 { + seatCount = int(parsed) + break + } else { + fmt.Printf("\033[31mPlease enter either 1 or 2!\033[00m\n") } - estimateChoice = estimates[choice] - break } + } - if estimateChoice == nil { - log.Fatal("illogical error, estimateChoice cannot be nil") - } + startCoord := centerToCoord(startGeocodeFeature.Center) + endCoord := centerToCoord(endGeocodeFeature.Center) + esReq := &uber.EstimateRequest{ + StartLatitude: startCoord.Lat, + StartLongitude: startCoord.Lng, + EndLatitude: endCoord.Lat, + EndLongitude: endCoord.Lng, + SeatCount: seatCount, + } - rreq := &uber.RideRequest{ - StartLatitude: startCoord.Lat, - StartLongitude: startCoord.Lng, - EndLatitude: endCoord.Lat, - EndLongitude: endCoord.Lng, - SeatCount: seatCount, - FareID: string(estimateChoice.UpfrontFare.Fare.ID), - ProductID: estimateChoice.Estimate.ProductID, + estimates, err := doUberEstimates(uberClient, esReq, spinr) + if err != nil { + log.Fatalf("estimate err: %v\n", err) + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetRowLine(true) + table.SetHeader([]string{ + "Choice", "Name", "Estimate", "Currency", + "Pickup ETA (minutes)", "Duration (minutes)", + }) + for i, est := range estimates { + et := est.Estimate + ufare := est.UpfrontFare + table.Append([]string{ + fmt.Sprintf("%d", i), + fmt.Sprintf("%s", et.LocalizedName), + fmt.Sprintf("%s", et.Estimate), + fmt.Sprintf("%s", et.CurrencyCode), + fmt.Sprintf("%.1f", ufare.PickupEstimateMinutes), + fmt.Sprintf("%.1f", et.DurationSeconds/60.0), + }) + } + table.Render() + + var estimateChoice *estimateAndUpfrontFarePair + + for { + fmt.Printf("Please enter the choice of your item or n to cancel ") + + lineIn := strings.TrimSpace(<-linesChan) + if strings.EqualFold(lineIn, repeatSentinel) { + return } - spinr.Start() - rres, err := uberClient.RequestRide(rreq) - spinr.Stop() + + choice, err := strconv.ParseUint(lineIn, 10, 32) if err != nil { - log.Fatalf("requestRide err: %v", err) + log.Fatalf("parsing choice err: %v", err) } + if choice < 0 || choice >= uint64(len(estimates)) { + log.Fatalf("choice must be >=0 && < %d", len(estimates)) + } + estimateChoice = estimates[choice] + break + } - fmt.Printf("\033[33mRide\033[00m\n") - dtable := tablewriter.NewWriter(os.Stdout) - dtable.SetHeader([]string{ - "Status", "RequestID", "Driver", "Rating", "Phone", "Shared", "Pickup ETA", "Destination ETA", - }) + if estimateChoice == nil { + log.Fatal("illogical error, estimateChoice cannot be nil") + } - locationDeref := func(loc *uber.Location) *uber.Location { - if loc == nil { - loc = new(uber.Location) - } - return loc + rreq := &uber.RideRequest{ + StartLatitude: startCoord.Lat, + StartLongitude: startCoord.Lng, + EndLatitude: endCoord.Lat, + EndLongitude: endCoord.Lng, + SeatCount: seatCount, + FareID: string(estimateChoice.UpfrontFare.Fare.ID), + ProductID: estimateChoice.Estimate.ProductID, + } + spinr.Start() + rres, err := uberClient.RequestRide(rreq) + spinr.Stop() + if err != nil { + log.Fatalf("requestRide err: %v", err) + } + + fmt.Printf("\033[33mRide\033[00m\n") + dtable := tablewriter.NewWriter(os.Stdout) + dtable.SetHeader([]string{ + "Status", "RequestID", "Driver", "Rating", "Phone", "Shared", "Pickup ETA", "Destination ETA", + }) + + locationDeref := func(loc *uber.Location) *uber.Location { + if loc == nil { + loc = new(uber.Location) } + return loc + } - dtable.Append([]string{ - fmt.Sprintf("%s", rres.Status), - rres.RequestID, - rres.Driver.Name, - fmt.Sprintf("%d", rres.Driver.Rating), - fmt.Sprintf("%s", rres.Driver.PhoneNumber), - fmt.Sprintf("%v", rres.Shared), - fmt.Sprintf("%.1f", locationDeref(rres.Pickup).ETAMinutes), - fmt.Sprintf("%.1f", locationDeref(rres.Destination).ETAMinutes), - }) - dtable.Render() + dtable.Append([]string{ + fmt.Sprintf("%s", rres.Status), + rres.RequestID, + rres.Driver.Name, + fmt.Sprintf("%d", rres.Driver.Rating), + fmt.Sprintf("%s", rres.Driver.PhoneNumber), + fmt.Sprintf("%v", rres.Shared), + fmt.Sprintf("%.1f", locationDeref(rres.Pickup).ETAMinutes), + fmt.Sprintf("%.1f", locationDeref(rres.Destination).ETAMinutes), + }) + dtable.Render() - vtable := tablewriter.NewWriter(os.Stdout) - fmt.Printf("\n\033[32mVehicle\033[00m\n") - vtable.SetHeader([]string{ - "Make", "Model", "License plate", "Picture", - }) - vtable.Append([]string{ - rres.Vehicle.Make, - rres.Vehicle.Model, - rres.Vehicle.LicensePlate, - rres.Vehicle.PictureURL, - }) - vtable.Render() + vtable := tablewriter.NewWriter(os.Stdout) + fmt.Printf("\n\033[32mVehicle\033[00m\n") + vtable.SetHeader([]string{ + "Make", "Model", "License plate", "Picture", + }) + vtable.Append([]string{ + rres.Vehicle.Make, + rres.Vehicle.Model, + rres.Vehicle.LicensePlate, + rres.Vehicle.PictureURL, + }) + vtable.Render() + +} + +func main() { + // Make log not print out time information as a prefix + log.SetFlags(0) + + var err error + mapboxClient, err = mapbox.NewClient() + if err != nil { + log.Fatal(err) } + + command.On("init", "authorizes and initializes your Uber account", &initCmd{}, nil) + command.On("order", "order your uber", &orderCmd{}, nil) + command.On("history", "view your trip history", &historyCmd{}, nil) + + command.ParseAndRun() } func doUberEstimates(uberC *uber.Client, esReq *uber.EstimateRequest, spinr *spinner.Spinner) ([]*estimateAndUpfrontFarePair, error) { @@ -368,45 +510,13 @@ func centerToCoord(center []float32) *coord { return &coord{Lat: float64(center[1]), Lng: float64(center[0])} } -func authorize() { - uberCredsDirPath, err := ensureUberCredsDirExists() - if err != nil { - log.Fatal(err) - } - - scopes := []string{ - oauth2.ScopeProfile, oauth2.ScopeRequest, - oauth2.ScopeHistory, oauth2.ScopePlaces, - oauth2.ScopeRequestReceipt, oauth2.ScopeDelivery, - } - - token, err := oauth2.AuthorizeByEnvApp(scopes...) - if err != nil { - log.Fatal(err) - } - - blob, err := json.Marshal(token) - if err != nil { - log.Fatal(err) - } - - credsPath := filepath.Join(uberCredsDirPath, "credentials.json") - f, err := os.Create(credsPath) - if err != nil { - log.Fatal(err) - } - - f.Write(blob) - log.Printf("Successfully saved your OAuth2.0 token to %q", credsPath) -} - func ensureUberCredsDirExists() (string, error) { wdir, err := os.Getwd() if err != nil { return "", err } - curDirPath := filepath.Join(wdir, ".uber") + curDirPath := filepath.Join(wdir, uberCredsDir) if err := os.MkdirAll(curDirPath, 0777); err != nil { return "", err }