├── .gitignore ├── COMPLETION_STATUS.md ├── LICENSE ├── README.md ├── cmd └── uber │ ├── Makefile │ ├── main.go │ └── testdata │ ├── end.json │ ├── ride.json │ └── start.json ├── example_test.go ├── oauth2 └── oauth2.go ├── uberhook ├── example_test.go └── uberhook.go └── v1 ├── client.go ├── deliveries.go ├── driver.go ├── errors.go ├── errors_test.go ├── history.go ├── maps.go ├── payment.go ├── places.go ├── prices.go ├── products.go ├── profile.go ├── receipts.go ├── rides.go ├── status.go ├── testdata ├── delivery-gizmo.json ├── driverProfile-TEST_TOKEN-1.json ├── driver_payments_0.json ├── driver_payments_10.json ├── driver_payments_2.json ├── driver_payments_4.json ├── driver_payments_6.json ├── driver_trips_0.json ├── driver_trips_10.json ├── driver_trips_2.json ├── driver_trips_4.json ├── driver_trips_6.json ├── fare-estimate-no-surge.json ├── fare-estimate-surge.json ├── list-payments-1.json ├── listProducts.json ├── map-b5512127-a134-4bf4-b1ba-fe9f48f56d9d.json ├── place-685-market.json ├── place-wallaby-way.json ├── price-estimate-1.json ├── product-a1111c8c-c720-46c3-8534-2fcdd730040d.json ├── profile-TEST_TOKEN-1.json ├── promo-code-pc1.json ├── receipt-b5512127-a134-4bf4-b1ba-fe9f48f56d9d.json ├── ride-a1111c8c-c720-46c3-8534-2fcdd730040d.json ├── time-estimate-1.json ├── trip-a1111c8c-c720-46c3-8534-2fcdd730040d.json └── trip-current.json ├── times.go ├── uber.go └── uber_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Text editor and miscellaneous files 27 | *.sw[op] 28 | *.DS_Store 29 | 30 | # Binary directories 31 | bin/ 32 | -------------------------------------------------------------------------------- /COMPLETION_STATUS.md: -------------------------------------------------------------------------------- 1 | # completion status 2 | Provides an almost one-to-one mapping of Uber REST API methods to those in this API client. 3 | 4 | ## Table of contents 5 | - [Rides API](#rides-api) 6 | # Rides API 7 | Uber API Method | API Method | Completion Status | Notes | Description 8 | ---|---|---|---|--- 9 | GET /authorize|oauth2.Authorize|✔️|Authentication|Allows you to redirect a user to the authorization URL for your application. See samples https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/cmd/uber/main.go#L111-L130 and https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/oauth2/oauth2.go#L209-L257 10 | POST /token|oauth2.Authorize|✔️|Authentication|The Login endpoint that allows you to authorize your application and get an access token using the authorization code or client credentials grant. See for example, see samples https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/cmd/uber/main.go#L111-L130 and https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/oauth2/oauth2.go#L209-L257 11 | PATCH /me|client.ApplyPromoCode|✔️||Allows you to apply a promocode to your account. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L202-L214 12 | GET /history|client.ListHistory|✔️||Retrieve the history of your trips. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L45-L80 13 | GET /payment-methods|client.ListPaymentMethods|✔️||Retrieves your payment methods. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L26-L43 14 | GET /places/{place_id}|client.Place|✔️||Retrieves either your HOME or WORK addresses, if set. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L230-L242 and https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L244-L256 15 | PUT /places/{place_id}|client.UpdatePlace|✔️||Updates either your HOME or WORK addresses. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L258-L273 and https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L275-L290 16 | GET /products|client.ListProducts|✔️||Allows you to get a list of products/car options at a location. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L439-L456 17 | GET /products/{product_id}|client.ProductByID|||Retrieves a product/car option by its ID. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L458-L470 18 | GET /estimates/price|client.EstimatePrice|||Returns an estimated price range for each product offered at a given location. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L114-L148 19 | GET /estimates/time|client.EstimateTime|✔️||Returns ETAs for all products currently available at a given location. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L150-L186 20 | GET /requests/estimate|client.UpfrontFare|✔️|Privileged scope, so needs an OAuth2.0 authorized client. This method is needed before you request a ride|Allows retrieve the upfront fare for all products currently available at a given location. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L317-L343 21 | POST /requests|client.RequestRide|✔️|Privileged scope, OAuth2.0 bearer token with the request scope. Requires you to pass in the FareID retrieved from client.UpfrontFare|See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L345-L368 22 | GET /requests/current|client.CurrentTrip|✔️|Requires privileged scope all_trips to be set|Retrieve details of an ongoing trip 23 | PATCH /requests/current||✖️|Unimplemented|Update an ongoing trip's destination 24 | DELETE /requests/current||✖️|Unimplemented|Cancel the ongoing trip 25 | GET /requests/{request_id}|client.TripByID|✔️|Requires privileged scope all_trips to be set|Retrieve the details of an ongoing or completed trip that was created by your app, by the trip's ID 26 | PATCH /requests/{request_id}||✖️|Unimplemented|Update the ongoing request's destination using the Ride Request endpoint 27 | DELETE /requests/{request_id}|||Unimplemented|Cancel the ongoing request on behalf of a rider 28 | GET /requests/{request_id}/map|client.OpenMap|✔️||This method is only available after a trip has been accepted by a driver and is in the accepted state|Opens up the map for an trip, to give a visual representation of a request. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L306-L315 29 | GET /requests/{request_id}/receipt|client.RequestReceipt|✔️|A privileged scope, whose output is only available after the requests.receipt_ready webhook notification is sent|The trip receipt may be adjusted after the requests.receipt_ready webhook is sent as finalized receipts can be delayed. See https://github.com/orijtech/uber/blob/1c064b69c7686b21ee5768468f39b900a2c1e8cb/example_test.go#L216-L228 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /cmd/uber/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for cross-compilation 2 | # 3 | OS := $(shell uname) 4 | BINDIR := ./bin 5 | MD5_TEXTFILE := $(BINDIR)/md5Sums.txt 6 | 7 | MAIN_FILE_DIR := ./ 8 | 9 | ifeq ($(OS), Darwin) 10 | MD5_UTIL = md5 11 | else 12 | MD5_UTIL = md5sum 13 | endif 14 | 15 | all: compileThemAll md5SumThemAll 16 | 17 | compileThemAll: armv5 armv6 armv7 armv8 darwin linux 18 | 19 | md5SumThemAll: 20 | rm -f $(MD5_TEXTFILE) 21 | find $(BINDIR) -type f -name "uber_*" -exec $(MD5_UTIL) {} >> $(MD5_TEXTFILE) \; 22 | cat $(MD5_TEXTFILE) 23 | 24 | armv5: 25 | CGO_ENABLED=0 GOOS=linux GOARM=5 GOARCH=arm go build -o $(BINDIR)/uber_armv5 $(MAIN_FILE_DIR) 26 | armv6: 27 | CGO_ENABLED=0 GOOS=linux GOARM=6 GOARCH=arm go build -o $(BINDIR)/uber_armv6 $(MAIN_FILE_DIR) 28 | armv7: 29 | CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -o $(BINDIR)/uber_armv7 $(MAIN_FILE_DIR) 30 | armv8: 31 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $(BINDIR)/uber_armv8 $(MAIN_FILE_DIR) 32 | darwin: 33 | CGO_ENABLED=0 GOOS=darwin go build -o $(BINDIR)/uber_darwin $(MAIN_FILE_DIR) 34 | linux: 35 | CGO_ENABLED=0 GOOS=linux go build -o $(BINDIR)/uber_linux $(MAIN_FILE_DIR) 36 | -------------------------------------------------------------------------------- /cmd/uber/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "errors" 21 | "flag" 22 | "fmt" 23 | "log" 24 | "os" 25 | "path/filepath" 26 | "sort" 27 | "strconv" 28 | "strings" 29 | "time" 30 | 31 | "github.com/orijtech/mapbox" 32 | "github.com/orijtech/uber/oauth2" 33 | "github.com/orijtech/uber/v1" 34 | 35 | "github.com/olekukonko/tablewriter" 36 | 37 | "github.com/odeke-em/cli-spinner" 38 | "github.com/odeke-em/command" 39 | "github.com/odeke-em/go-utils/fread" 40 | "github.com/odeke-em/semalim" 41 | ) 42 | 43 | const repeatSentinel = "n" 44 | 45 | var mapboxClient *mapbox.Client 46 | 47 | type initCmd struct { 48 | } 49 | 50 | var _ command.Cmd = (*initCmd)(nil) 51 | 52 | type paymentsCmd struct { 53 | } 54 | 55 | var _ command.Cmd = (*paymentsCmd)(nil) 56 | 57 | func (p *paymentsCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { 58 | return fs 59 | } 60 | 61 | func (p *paymentsCmd) Run(args []string, defaults map[string]*flag.Flag) { 62 | credsPath := credsMustExist() 63 | client, err := uberClientFromFile(credsPath) 64 | exitIfErr(err) 65 | 66 | listings, err := client.ListPaymentMethods() 67 | exitIfErr(err) 68 | 69 | table := tablewriter.NewWriter(os.Stdout) 70 | table.SetRowLine(true) 71 | 72 | table.SetHeader([]string{ 73 | "Method", "ID", "Description", "LastUsed", 74 | }) 75 | 76 | for _, method := range listings.Methods { 77 | lastUsedTok := "" 78 | if method.ID == listings.LastUsedID { 79 | lastUsedTok = "✔️" 80 | } 81 | table.Append([]string{ 82 | fmt.Sprintf("%s", method.PaymentMethod), 83 | method.ID, 84 | method.Description, 85 | lastUsedTok, 86 | }) 87 | } 88 | table.Render() 89 | } 90 | 91 | func (a *initCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { 92 | return fs 93 | } 94 | 95 | func exitIfErr(err error) { 96 | if err != nil { 97 | fmt.Fprintf(os.Stderr, "%v\n", err) 98 | os.Exit(-1) 99 | } 100 | } 101 | 102 | const ( 103 | uberCredsDir = ".uber" 104 | credentialsOutFile = "credentials.json" 105 | ) 106 | 107 | func (a *initCmd) Run(args []string, defaults map[string]*flag.Flag) { 108 | uberCredsDirPath, err := ensureUberCredsDirExists() 109 | if err != nil { 110 | exitIfErr(fmt.Errorf("init: os.MkdirAll(%q) err=%q", err)) 111 | } 112 | 113 | scopes := []string{ 114 | oauth2.ScopeProfile, oauth2.ScopeRequest, 115 | oauth2.ScopeHistory, oauth2.ScopePlaces, 116 | oauth2.ScopeRequestReceipt, oauth2.ScopeDelivery, 117 | 118 | // To allow for driver information retrieval 119 | oauth2.ScopePartnerAccounts, 120 | oauth2.ScopePartnerPayments, 121 | oauth2.ScopePartnerTrips, 122 | 123 | oauth2.ScopeAllTrips, 124 | } 125 | 126 | token, err := oauth2.AuthorizeByEnvApp(scopes...) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | 131 | blob, err := json.Marshal(token) 132 | if err != nil { 133 | log.Fatal(err) 134 | } 135 | 136 | credsPath := filepath.Join(uberCredsDirPath, credentialsOutFile) 137 | f, err := os.Create(credsPath) 138 | if err != nil { 139 | log.Fatal(err) 140 | } 141 | 142 | f.Write(blob) 143 | log.Printf("Successfully saved your OAuth2.0 token to %q", credsPath) 144 | } 145 | 146 | func uberClientFromFile(path string) (*uber.Client, error) { 147 | return uber.NewClientFromOAuth2File(path) 148 | } 149 | 150 | type historyCmd struct { 151 | maxPage int 152 | limitPerPage int 153 | noPrompt bool 154 | pageOffset int 155 | throttleStr string 156 | } 157 | 158 | var _ command.Cmd = (*historyCmd)(nil) 159 | 160 | func (h *historyCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { 161 | fs.IntVar(&h.maxPage, "max-page", 4, "the maximum number of pages to show") 162 | fs.BoolVar(&h.noPrompt, "no-prompt", false, "if set, do not prompt") 163 | fs.IntVar(&h.limitPerPage, "limit-per-page", 0, "limits the number of items retrieved per page") 164 | fs.IntVar(&h.pageOffset, "page-offset", 0, "positions where to start pagination from") 165 | fs.StringVar(&h.throttleStr, "throttle", "", "the throttle duration e.g 8s, 10m, 7ms as per https://golang.org/pkg/time/#ParseDuration") 166 | return fs 167 | } 168 | 169 | func credsMustExist() string { 170 | wdir, err := os.Getwd() 171 | if err != nil { 172 | exitIfErr(fmt.Errorf("credentials: os.Getwd err=%q", err)) 173 | } 174 | 175 | fullPath := filepath.Join(wdir, uberCredsDir, credentialsOutFile) 176 | _, err = os.Stat(fullPath) 177 | exitIfErr(err) 178 | 179 | return fullPath 180 | } 181 | 182 | func (h *historyCmd) Run(args []string, defaults map[string]*flag.Flag) { 183 | credsPath := credsMustExist() 184 | client, err := uberClientFromFile(credsPath) 185 | exitIfErr(err) 186 | 187 | // If an invalid duration is passed, it'll return 188 | // the zero value which is the same as passing in nothing. 189 | throttle, _ := time.ParseDuration(h.throttleStr) 190 | 191 | pagesChan, _, err := client.ListHistory(&uber.Pager{ 192 | LimitPerPage: int64(h.limitPerPage), 193 | MaxPages: int64(h.maxPage), 194 | StartOffset: int64(h.pageOffset), 195 | 196 | ThrottleDuration: throttle, 197 | }) 198 | 199 | exitIfErr(err) 200 | 201 | for page := range pagesChan { 202 | if page.Err != nil { 203 | fmt.Printf("Page: #%d err: %v\n", page.PageNumber, page.Err) 204 | continue 205 | } 206 | 207 | table := tablewriter.NewWriter(os.Stdout) 208 | table.SetRowLine(true) 209 | table.SetHeader([]string{ 210 | "Trip #", "City", "Date", 211 | "Duration", "Miles", "RequestID", 212 | }) 213 | fmt.Printf("\n\033[32mPage: #%d\033[00m\n", page.PageNumber+1) 214 | for i, trip := range page.Trips { 215 | startCity := trip.StartCity 216 | startDate := time.Unix(trip.StartTimeUnix, 0) 217 | endDate := time.Unix(trip.EndTimeUnix, 0) 218 | table.Append([]string{ 219 | fmt.Sprintf("%d", i+1), 220 | fmt.Sprintf("%s", startCity.Name), 221 | fmt.Sprintf("%s", startDate.Format("2006/01/02 15:04:05 MST")), 222 | fmt.Sprintf("%s", endDate.Sub(startDate)), 223 | fmt.Sprintf("%.3f", trip.DistanceMiles), 224 | fmt.Sprintf("%s", trip.RequestID), 225 | }) 226 | } 227 | table.Render() 228 | } 229 | } 230 | 231 | type profileCmd struct { 232 | } 233 | 234 | var _ command.Cmd = (*profileCmd)(nil) 235 | 236 | func (p *profileCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { 237 | return fs 238 | } 239 | 240 | func (p *profileCmd) Run(args []string, defaults map[string]*flag.Flag) { 241 | credsPath := credsMustExist() 242 | uberClient, err := uberClientFromFile(credsPath) 243 | exitIfErr(err) 244 | 245 | myProfile, err := uberClient.RetrieveMyProfile() 246 | if err != nil { 247 | log.Fatal(err) 248 | } 249 | 250 | table := tablewriter.NewWriter(os.Stdout) 251 | table.SetRowLine(true) 252 | table.SetHeader([]string{"Key", "Value"}) 253 | 254 | blob, err := json.Marshal(myProfile) 255 | if err != nil { 256 | log.Fatalf("serializing profile: %v", err) 257 | } 258 | kvMap := make(map[string]interface{}) 259 | if err := json.Unmarshal(blob, &kvMap); err != nil { 260 | log.Fatalf("deserializing kvMap: %v", err) 261 | } 262 | 263 | keys := make([]string, 0, len(kvMap)) 264 | for key := range kvMap { 265 | keys = append(keys, key) 266 | } 267 | sort.Strings(keys) 268 | 269 | for _, key := range keys { 270 | value := kvMap[key] 271 | table.Append([]string{ 272 | key, fmt.Sprintf("%v", value), 273 | }) 274 | } 275 | table.Render() 276 | } 277 | 278 | type orderCmd struct { 279 | } 280 | 281 | var _ command.Cmd = (*orderCmd)(nil) 282 | 283 | func (o *orderCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { 284 | return fs 285 | } 286 | 287 | func (o *orderCmd) Run(args []string, defaults map[string]*flag.Flag) { 288 | credsPath := credsMustExist() 289 | uberClient, err := uberClientFromFile(credsPath) 290 | exitIfErr(err) 291 | 292 | spinr := spinner.New(10) 293 | var startGeocodeFeature, endGeocodeFeature mapbox.GeocodeFeature 294 | items := [...]struct { 295 | ft *mapbox.GeocodeFeature 296 | prompt string 297 | }{ 298 | 0: {&startGeocodeFeature, "Start Point: "}, 299 | 1: {&endGeocodeFeature, "End Point: "}, 300 | } 301 | 302 | linesChan := fread.Fread(os.Stdin) 303 | for i, item := range items { 304 | for { 305 | geocodeFeature, query, err := doSearch(item.prompt, linesChan, "n", spinr) 306 | if err == nil { 307 | *item.ft = *geocodeFeature 308 | break 309 | } 310 | 311 | switch err { 312 | case errRepeat: 313 | fmt.Printf("\033[32mSearching again *\033[00m\n") 314 | continue 315 | case errNoMatchFound: 316 | fmt.Printf("No matches found found for %q. Try again? (y/N) ", query) 317 | continueResponse := strings.TrimSpace(<-linesChan) 318 | if strings.HasPrefix(strings.ToLower(continueResponse), "y") { 319 | continue 320 | } 321 | return 322 | default: 323 | // Otherwise an unhandled error 324 | log.Fatalf("%d: search err: %v; prompt=%q", i, err, item.prompt) 325 | } 326 | } 327 | } 328 | 329 | var seatCount int = 2 330 | for { 331 | fmt.Printf("Seat count: 1 or 2 (default 2) ") 332 | seatCountLine := strings.TrimSpace(<-linesChan) 333 | if seatCountLine == "" { 334 | seatCount = 2 335 | break 336 | } else { 337 | parsed, err := strconv.ParseInt(seatCountLine, 10, 32) 338 | if err != nil { 339 | log.Fatalf("seatCount parsing err: %v", err) 340 | } 341 | if parsed >= 1 && parsed <= 2 { 342 | seatCount = int(parsed) 343 | break 344 | } else { 345 | fmt.Printf("\033[31mPlease enter either 1 or 2!\033[00m\n") 346 | } 347 | } 348 | } 349 | 350 | startCoord := centerToCoord(startGeocodeFeature.Center) 351 | endCoord := centerToCoord(endGeocodeFeature.Center) 352 | esReq := &uber.EstimateRequest{ 353 | StartLatitude: startCoord.Lat, 354 | StartLongitude: startCoord.Lng, 355 | EndLatitude: endCoord.Lat, 356 | EndLongitude: endCoord.Lng, 357 | SeatCount: seatCount, 358 | } 359 | 360 | estimates, err := doUberEstimates(uberClient, esReq, spinr) 361 | if err != nil { 362 | log.Fatalf("estimate err: %v\n", err) 363 | } 364 | 365 | table := tablewriter.NewWriter(os.Stdout) 366 | table.SetRowLine(true) 367 | table.SetHeader([]string{ 368 | "Choice", "Name", "Estimate", "Currency", 369 | "Pickup ETA (minutes)", "Duration (minutes)", 370 | }) 371 | for i, est := range estimates { 372 | et := est.Estimate 373 | ufare := est.UpfrontFare 374 | table.Append([]string{ 375 | fmt.Sprintf("%d", i), 376 | fmt.Sprintf("%s", et.LocalizedName), 377 | fmt.Sprintf("%s", et.Estimate), 378 | fmt.Sprintf("%s", et.CurrencyCode), 379 | fmt.Sprintf("%.1f", ufare.PickupEstimateMinutes), 380 | fmt.Sprintf("%.1f", et.DurationSeconds/60.0), 381 | }) 382 | } 383 | table.Render() 384 | 385 | var estimateChoice *estimateAndUpfrontFarePair 386 | 387 | for { 388 | fmt.Printf("Please enter the choice of your item or n to cancel ") 389 | 390 | lineIn := strings.TrimSpace(<-linesChan) 391 | if strings.EqualFold(lineIn, repeatSentinel) { 392 | return 393 | } 394 | 395 | choice, err := strconv.ParseUint(lineIn, 10, 32) 396 | if err != nil { 397 | log.Fatalf("parsing choice err: %v", err) 398 | } 399 | if choice < 0 || choice >= uint64(len(estimates)) { 400 | log.Fatalf("choice must be >=0 && < %d", len(estimates)) 401 | } 402 | estimateChoice = estimates[choice] 403 | break 404 | } 405 | 406 | if estimateChoice == nil { 407 | log.Fatal("illogical error, estimateChoice cannot be nil") 408 | } 409 | 410 | rreq := &uber.RideRequest{ 411 | StartLatitude: startCoord.Lat, 412 | StartLongitude: startCoord.Lng, 413 | EndLatitude: endCoord.Lat, 414 | EndLongitude: endCoord.Lng, 415 | SeatCount: seatCount, 416 | FareID: string(estimateChoice.UpfrontFare.Fare.ID), 417 | ProductID: estimateChoice.Estimate.ProductID, 418 | } 419 | spinr.Start() 420 | rres, err := uberClient.RequestRide(rreq) 421 | spinr.Stop() 422 | if err != nil { 423 | log.Fatalf("requestRide err: %v", err) 424 | } 425 | 426 | fmt.Printf("\033[33mRide\033[00m\n") 427 | dtable := tablewriter.NewWriter(os.Stdout) 428 | dtable.SetHeader([]string{ 429 | "Status", "RequestID", "Driver", "Rating", "Phone", "Shared", "Pickup ETA", "Destination ETA", 430 | }) 431 | 432 | locationDeref := func(loc *uber.Location) *uber.Location { 433 | if loc == nil { 434 | loc = new(uber.Location) 435 | } 436 | return loc 437 | } 438 | 439 | dtable.Append([]string{ 440 | fmt.Sprintf("%s", rres.Status), 441 | rres.RequestID, 442 | rres.Driver.Name, 443 | fmt.Sprintf("%d", rres.Driver.Rating), 444 | fmt.Sprintf("%s", rres.Driver.PhoneNumber), 445 | fmt.Sprintf("%v", rres.Shared), 446 | fmt.Sprintf("%.1f", locationDeref(rres.Pickup).ETAMinutes), 447 | fmt.Sprintf("%.1f", locationDeref(rres.Destination).ETAMinutes), 448 | }) 449 | dtable.Render() 450 | 451 | vtable := tablewriter.NewWriter(os.Stdout) 452 | fmt.Printf("\n\033[32mVehicle\033[00m\n") 453 | vtable.SetHeader([]string{ 454 | "Make", "Model", "License plate", "Picture", 455 | }) 456 | vtable.Append([]string{ 457 | rres.Vehicle.Make, 458 | rres.Vehicle.Model, 459 | rres.Vehicle.LicensePlate, 460 | rres.Vehicle.PictureURL, 461 | }) 462 | vtable.Render() 463 | 464 | } 465 | 466 | func main() { 467 | // Make log not print out time information as a prefix 468 | log.SetFlags(0) 469 | 470 | var err error 471 | mapboxClient, err = mapbox.NewClient() 472 | if err != nil { 473 | log.Fatal(err) 474 | } 475 | 476 | command.On("init", "authorizes and initializes your Uber account", &initCmd{}, nil) 477 | command.On("order", "order your uber", &orderCmd{}, nil) 478 | command.On("history", "view your trip history", &historyCmd{}, nil) 479 | command.On("payments", "list your payments methods", &paymentsCmd{}, nil) 480 | command.On("profile", "details about your profile", &profileCmd{}, nil) 481 | 482 | command.ParseAndRun() 483 | } 484 | 485 | func doUberEstimates(uberC *uber.Client, esReq *uber.EstimateRequest, spinr *spinner.Spinner) ([]*estimateAndUpfrontFarePair, error) { 486 | spinr.Start() 487 | estimatesPageChan, cancelPaging, err := uberC.EstimatePrice(esReq) 488 | spinr.Stop() 489 | if err != nil { 490 | return nil, err 491 | } 492 | 493 | var allEstimates []*uber.PriceEstimate 494 | for page := range estimatesPageChan { 495 | if page.Err == nil { 496 | allEstimates = append(allEstimates, page.Estimates...) 497 | } 498 | if len(allEstimates) >= 400 { 499 | cancelPaging() 500 | } 501 | } 502 | 503 | spinr.Start() 504 | defer spinr.Stop() 505 | 506 | jobsBench := make(chan semalim.Job) 507 | go func() { 508 | defer close(jobsBench) 509 | 510 | for i, estimate := range allEstimates { 511 | jobsBench <- &lookupFare{ 512 | client: uberC, 513 | id: i, 514 | estimate: estimate, 515 | esReq: &uber.EstimateRequest{ 516 | StartLatitude: esReq.StartLatitude, 517 | StartLongitude: esReq.StartLongitude, 518 | StartPlace: esReq.StartPlace, 519 | EndPlace: esReq.EndPlace, 520 | EndLatitude: esReq.EndLatitude, 521 | EndLongitude: esReq.EndLongitude, 522 | SeatCount: esReq.SeatCount, 523 | ProductID: estimate.ProductID, 524 | }, 525 | } 526 | } 527 | }() 528 | 529 | var pairs []*estimateAndUpfrontFarePair 530 | resChan := semalim.Run(jobsBench, 5) 531 | for res := range resChan { 532 | // No ordering required so can just retrieve and add results in 533 | if retr := res.Value().(*estimateAndUpfrontFarePair); retr != nil { 534 | pairs = append(pairs, retr) 535 | } 536 | } 537 | 538 | return pairs, nil 539 | } 540 | 541 | var ( 542 | errNoMatchFound = errors.New("no matches found") 543 | errRepeat = errors.New("repeat match") 544 | ) 545 | 546 | func doSearch(prompt string, linesChan <-chan string, repeatSentinel string, spinr *spinner.Spinner) (*mapbox.GeocodeFeature, string, error) { 547 | fmt.Printf(prompt) 548 | 549 | query := strings.TrimSpace(<-linesChan) 550 | if query == "" { 551 | return nil, query, errRepeat 552 | } 553 | spinr.Start() 554 | matches, err := mapboxClient.LookupPlace(context.Background(), query) 555 | spinr.Stop() 556 | if err != nil { 557 | return nil, query, err 558 | } 559 | 560 | if len(matches.Features) == 0 { 561 | return nil, query, errNoMatchFound 562 | } 563 | 564 | table := tablewriter.NewWriter(os.Stdout) 565 | table.SetHeader([]string{"Choice", "Name", "Relevance", "Latitude", "Longitude"}) 566 | table.SetRowLine(true) 567 | 568 | for i, feat := range matches.Features { 569 | coord := centerToCoord(feat.Center) 570 | table.Append([]string{ 571 | fmt.Sprintf("%d", i), 572 | fmt.Sprintf("%s", feat.PlaceName), 573 | fmt.Sprintf("%.2f%%", feat.Relevance*100), 574 | fmt.Sprintf("%f", coord.Lat), 575 | fmt.Sprintf("%f", coord.Lng), 576 | }) 577 | } 578 | table.Render() 579 | 580 | fmt.Printf("Please enter your choice by numeric key or (%v) to search again: ", repeatSentinel) 581 | lineIn := strings.TrimSpace(<-linesChan) 582 | if strings.EqualFold(lineIn, repeatSentinel) { 583 | return nil, query, errRepeat 584 | } 585 | 586 | choice, err := strconv.ParseUint(lineIn, 10, 32) 587 | if err != nil { 588 | return nil, query, err 589 | } 590 | if choice < 0 || choice >= uint64(len(matches.Features)) { 591 | return nil, query, fmt.Errorf("choice must be >=0 && < %d", len(matches.Features)) 592 | } 593 | return matches.Features[choice], query, nil 594 | } 595 | 596 | func input() string { 597 | var str string 598 | fmt.Scanln(os.Stdin, &str) 599 | return str 600 | } 601 | 602 | type coord struct { 603 | Lat, Lng float64 604 | } 605 | 606 | func centerToCoord(center []float32) *coord { 607 | return &coord{Lat: float64(center[1]), Lng: float64(center[0])} 608 | } 609 | 610 | func ensureUberCredsDirExists() (string, error) { 611 | wdir, err := os.Getwd() 612 | if err != nil { 613 | return "", err 614 | } 615 | 616 | curDirPath := filepath.Join(wdir, uberCredsDir) 617 | if err := os.MkdirAll(curDirPath, 0777); err != nil { 618 | return "", err 619 | } 620 | return curDirPath, nil 621 | } 622 | 623 | type lookupFare struct { 624 | id int 625 | estimate *uber.PriceEstimate 626 | esReq *uber.EstimateRequest 627 | client *uber.Client 628 | } 629 | 630 | var _ semalim.Job = (*lookupFare)(nil) 631 | 632 | func (lf *lookupFare) Id() interface{} { 633 | return lf.id 634 | } 635 | 636 | func (lf *lookupFare) Do() (interface{}, error) { 637 | upfrontFare, err := lookupUpfrontFare(lf.client, &uber.EstimateRequest{ 638 | StartLatitude: lf.esReq.StartLatitude, 639 | StartLongitude: lf.esReq.StartLongitude, 640 | StartPlace: lf.esReq.StartPlace, 641 | EndPlace: lf.esReq.EndPlace, 642 | EndLatitude: lf.esReq.EndLatitude, 643 | EndLongitude: lf.esReq.EndLongitude, 644 | SeatCount: lf.esReq.SeatCount, 645 | ProductID: lf.estimate.ProductID, 646 | }) 647 | 648 | return &estimateAndUpfrontFarePair{Estimate: lf.estimate, UpfrontFare: upfrontFare}, err 649 | } 650 | 651 | type estimateAndUpfrontFarePair struct { 652 | Estimate *uber.PriceEstimate `json:"estimate"` 653 | UpfrontFare *uber.UpfrontFare `json:"upfront_fare"` 654 | } 655 | 656 | func lookupUpfrontFare(c *uber.Client, rr *uber.EstimateRequest) (*uber.UpfrontFare, error) { 657 | // Otherwise it is time to get the estimate of the fare 658 | return c.UpfrontFare(rr) 659 | } 660 | -------------------------------------------------------------------------------- /cmd/uber/testdata/end.json: -------------------------------------------------------------------------------- 1 | [{"id":"place.9080100702660390","type":"Feature","text":"Edmonton","place_name":"Edmonton, Alberta, Canada","relevance":0.49,"properties":{"wikidata":"Q2096"},"context":[{"id":"region.219693","text":"Alberta","short_code":"CA-AB","wikidata":"Q1951"},{"id":"country.3179","text":"Canada","short_code":"ca","wikidata":"Q16"}],"bbox":[-113.7138,53.395603,-113.271645,53.715984],"center":[-113.5065,53.5344],"geometry":{"type":"Point","coordinates":[-113.5065,53.5344]},"attribution":""},{"id":"place.12628624761375070","type":"Feature","text":"Southgate","place_name":"Southgate, Michigan, United States","relevance":0.49,"properties":{"wikidata":"Q119706"},"context":[{"id":"region.219112","text":"Michigan","short_code":"US-MI","wikidata":"Q1166"},{"id":"country.3145","text":"United States","short_code":"us","wikidata":"Q30"}],"bbox":[-83.23253,42.18428,-83.179596,42.22801],"center":[-83.2083,42.2032],"geometry":{"type":"Point","coordinates":[-83.2083,42.2032]},"attribution":""},{"id":"neighborhood.293894","type":"Feature","text":"Southgate","place_name":"Southgate, Hayward, California 94545, United States","relevance":0.49,"properties":{},"context":[{"id":"postcode.6500720573961840","text":"94545","short_code":"","wikidata":""},{"id":"place.10282886204411860","text":"Hayward","short_code":"","wikidata":"Q491114"},{"id":"region.3591","text":"California","short_code":"US-CA","wikidata":"Q99"},{"id":"country.3145","text":"United States","short_code":"us","wikidata":"Q30"}],"bbox":[-122.11031,37.63093,-122.08442,37.65673],"center":[-122.1,37.64],"geometry":{"type":"Point","coordinates":[-122.1,37.64]},"attribution":""},{"id":"neighborhood.2100987","type":"Feature","text":"Southgate","place_name":"Southgate, Lakewood, Washington 98499, United States","relevance":0.49,"properties":{},"context":[{"id":"postcode.10053733604137270","text":"98499","short_code":"","wikidata":""},{"id":"place.9773350605366720","text":"Lakewood","short_code":"","wikidata":"Q983791"},{"id":"region.213154","text":"Washington","short_code":"US-WA","wikidata":"Q1223"},{"id":"country.3145","text":"United States","short_code":"us","wikidata":"Q30"}],"bbox":[-122.49517,47.154114,-122.466736,47.172024],"center":[-122.48,47.16],"geometry":{"type":"Point","coordinates":[-122.48,47.16]},"attribution":""},{"id":"neighborhood.2106382","type":"Feature","text":"Southgate","place_name":"Southgate, Milwaukee, Wisconsin 53215, United States","relevance":0.49,"properties":{},"context":[{"id":"postcode.8707108028172880","text":"53215","short_code":"","wikidata":""},{"id":"place.7506480977217990","text":"Milwaukee","short_code":"","wikidata":"Q37836"},{"id":"region.219024","text":"Wisconsin","short_code":"US-WI","wikidata":"Q1537"},{"id":"country.3145","text":"United States","short_code":"us","wikidata":"Q30"}],"bbox":[-87.95305,42.98107,-87.93833,42.992683],"center":[-87.95,42.99],"geometry":{"type":"Point","coordinates":[-87.95,42.99]},"attribution":""}] -------------------------------------------------------------------------------- /cmd/uber/testdata/ride.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_id": "17cb78a7-b672-4d34-a288-a6c6e44d5315", 3 | "request_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 4 | "status": "accepted", 5 | "surge_multiplier": 1.0, 6 | "shared": true, 7 | "driver": { 8 | "phone_number": "(415)555-1212", 9 | "sms_number": "(415)555-1212", 10 | "rating": 5, 11 | "picture_url": "https://d1w2poirtb3as9.cloudfront.net/img.jpeg", 12 | "name": "Bob" 13 | }, 14 | "vehicle": { 15 | "make": "Bugatti", 16 | "model": "Veyron", 17 | "license_plate": "I<3Uber", 18 | "picture_url": "https://d1w2poirtb3as9.cloudfront.net/car.jpeg" 19 | }, 20 | "location": { 21 | "latitude": 37.3382129093, 22 | "longitude": -121.8863287568, 23 | "bearing": 328 24 | }, 25 | "pickup": { 26 | "latitude": 37.3303463, 27 | "longitude": -121.8890484, 28 | "eta": 5 29 | }, 30 | "destination": { 31 | "latitude": 37.6213129, 32 | "longitude": -122.3789554, 33 | "eta": 19 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/uber/testdata/start.json: -------------------------------------------------------------------------------- 1 | [{"id":"poi.15555644443896030","type":"Feature","text":"West Edmonton Mall","place_name":"West Edmonton Mall, 8882 170 St NW, Edmonton, Alberta T5T 4M2, Canada","relevance":0.99,"properties":{"address":"8882 170 St NW","category":"landmark","landmark":true,"maki":"monument","tel":"(780) 444-5321","wikidata":"Q1044789"},"context":[{"id":"neighborhood.5879030753669280","text":"Lynnwood","short_code":"","wikidata":""},{"id":"postcode.12520904101019170","text":"T5T 4M2","short_code":"","wikidata":""},{"id":"place.9080100702660390","text":"Edmonton","short_code":"","wikidata":"Q2096"},{"id":"region.219693","text":"Alberta","short_code":"CA-AB","wikidata":"Q1951"},{"id":"country.3179","text":"Canada","short_code":"ca","wikidata":"Q16"}],"bbox":null,"center":[-113.62278,53.52222],"geometry":{"type":"Point","coordinates":[-113.62278,53.52222]},"attribution":""},{"id":"poi.4514440791940890","type":"Feature","text":"West Edmonton Mall Transit Centre","place_name":"West Edmonton Mall Transit Centre, Edmonton, Alberta T5T 4J2, Canada","relevance":0.99,"properties":{"category":"bus station, bus stop, bus","landmark":true,"maki":"bus"},"context":[{"id":"neighborhood.5879030753669280","text":"Lynnwood","short_code":"","wikidata":""},{"id":"postcode.12420303953212740","text":"T5T 4J2","short_code":"","wikidata":""},{"id":"place.9080100702660390","text":"Edmonton","short_code":"","wikidata":"Q2096"},{"id":"region.219693","text":"Alberta","short_code":"CA-AB","wikidata":"Q1951"},{"id":"country.3179","text":"Canada","short_code":"ca","wikidata":"Q16"}],"bbox":null,"center":[-113.622574,53.520397],"geometry":{"type":"Point","coordinates":[-113.622574,53.520397]},"attribution":""},{"id":"poi.10581969664216370","type":"Feature","text":"Malmo Elementary School","place_name":"Malmo Elementary School, 4716 115 St NW, Edmonton, Alberta T6H 0E5, Canada","relevance":0.6536667,"properties":{"address":"4716 115 St NW","category":"primary school, secondary school, elementary, school","landmark":true,"maki":"school","tel":"(780) 434-1362"},"context":[{"id":"neighborhood.18827898930330520","text":"Lendrum Place","short_code":"","wikidata":""},{"id":"postcode.10569297065954110","text":"T6H 0E5","short_code":"","wikidata":""},{"id":"place.9080100702660390","text":"Edmonton","short_code":"","wikidata":"Q2096"},{"id":"region.219693","text":"Alberta","short_code":"CA-AB","wikidata":"Q1951"},{"id":"country.3179","text":"Canada","short_code":"ca","wikidata":"Q16"}],"bbox":null,"center":[-113.52909,53.484833],"geometry":{"type":"Point","coordinates":[-113.52909,53.484833]},"attribution":""},{"id":"poi.9160354503027690","type":"Feature","text":"Malcolm Tweddle Elementary School","place_name":"Malcolm Tweddle Elementary School, 2340 Millbourne Rd West NW, Edmonton, Alberta T6K 1H3, Canada","relevance":0.6536667,"properties":{"address":"2340 Millbourne Rd West NW","category":"primary school, secondary school, elementary, school","landmark":true,"maki":"school","tel":"(780) 462-3270"},"context":[{"id":"neighborhood.16818569417694470","text":"Argyll","short_code":"","wikidata":""},{"id":"postcode.9167295351027780","text":"T6K 1H3","short_code":"","wikidata":""},{"id":"place.9080100702660390","text":"Edmonton","short_code":"","wikidata":"Q2096"},{"id":"region.219693","text":"Alberta","short_code":"CA-AB","wikidata":"Q1951"},{"id":"country.3179","text":"Canada","short_code":"ca","wikidata":"Q16"}],"bbox":null,"center":[-113.45801,53.477253],"geometry":{"type":"Point","coordinates":[-113.45801,53.477253]},"attribution":""},{"id":"poi.10016908438986270","type":"Feature","text":"Malt's Lounge","place_name":"Malt's Lounge, 10155 105 St NW, Edmonton, Alberta T5J 1C9, Canada","relevance":0.6536667,"properties":{"address":"10155 105 St NW","category":"bar, alcohol","landmark":true,"maki":"bar","tel":"(780) 423-4811"},"context":[{"id":"neighborhood.2475627669682630","text":"Downtown Edmonton","short_code":"","wikidata":""},{"id":"postcode.10004308389782740","text":"T5J 1C9","short_code":"","wikidata":""},{"id":"place.9080100702660390","text":"Edmonton","short_code":"","wikidata":"Q2096"},{"id":"region.219693","text":"Alberta","short_code":"CA-AB","wikidata":"Q1951"},{"id":"country.3179","text":"Canada","short_code":"ca","wikidata":"Q16"}],"bbox":null,"center":[-113.50084,53.54219],"geometry":{"type":"Point","coordinates":[-113.50084,53.54219]},"attribution":""}] 2 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber_test 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "os" 21 | "time" 22 | 23 | "github.com/orijtech/uber/v1" 24 | ) 25 | 26 | func Example_client_ListPaymentMethods() { 27 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | listings, err := client.ListPaymentMethods() 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | fmt.Printf("LastUsedD: %v\n", listings.LastUsedID) 38 | 39 | for i, method := range listings.Methods { 40 | fmt.Printf("#%d: ID: %q PaymentMethod: %q Description: %q\n", 41 | i, method.ID, method.PaymentMethod, method.Description) 42 | } 43 | } 44 | 45 | func Example_client_ListHistory() { 46 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | pagesChan, cancelPaging, err := client.ListHistory(&uber.Pager{ 52 | MaxPages: 4, 53 | LimitPerPage: 10, 54 | StartOffset: 0, 55 | }) 56 | 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | for page := range pagesChan { 62 | if page.Err != nil { 63 | fmt.Printf("Page: #%d err: %v\n", page.PageNumber, page.Err) 64 | continue 65 | } 66 | 67 | fmt.Printf("Page: #%d\n\n", page.PageNumber) 68 | for i, trip := range page.Trips { 69 | startCity := trip.StartCity 70 | if startCity.Name == "Tokyo" { 71 | fmt.Printf("aha found the first Tokyo trip, canceling any more requests!: %#v\n", trip) 72 | cancelPaging() 73 | break 74 | } 75 | 76 | // Otherwise, continue listing 77 | fmt.Printf("Trip: #%d ==> %#v place: %#v\n", i, trip, startCity) 78 | } 79 | } 80 | } 81 | 82 | func Example_client_ListAllMyHistory() { 83 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | 88 | pagesChan, cancelPaging, err := client.ListAllMyHistory() 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | for page := range pagesChan { 94 | if page.Err != nil { 95 | fmt.Printf("Page: #%d err: %v\n", page.PageNumber, page.Err) 96 | continue 97 | } 98 | 99 | fmt.Printf("Page: #%d\n\n", page.PageNumber) 100 | for i, trip := range page.Trips { 101 | startCity := trip.StartCity 102 | if startCity.Name == "Edmonton" { 103 | fmt.Printf("aha found the trip from Edmonton, canceling the rest!: %#v\n", trip) 104 | cancelPaging() 105 | break 106 | } 107 | 108 | // Otherwise, continue listing 109 | fmt.Printf("Trip: #%d ==> %#v place: %#v\n", i, trip, startCity) 110 | } 111 | } 112 | } 113 | 114 | func Example_client_EstimatePrice() { 115 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 116 | if err != nil { 117 | log.Fatal(err) 118 | } 119 | 120 | estimatesPageChan, cancelPaging, err := client.EstimatePrice(&uber.EstimateRequest{ 121 | StartLatitude: 37.7752315, 122 | EndLatitude: 37.7752415, 123 | StartLongitude: -122.418075, 124 | EndLongitude: -122.518075, 125 | SeatCount: 2, 126 | }) 127 | 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | 132 | itemCount := uint64(0) 133 | for page := range estimatesPageChan { 134 | if page.Err != nil { 135 | fmt.Printf("PageNumber: #%d err: %v", page.PageNumber, page.Err) 136 | continue 137 | } 138 | 139 | for i, estimate := range page.Estimates { 140 | itemCount += 1 141 | fmt.Printf("Estimate: #%d ==> %#v\n", i, estimate) 142 | } 143 | 144 | if itemCount >= 23 { 145 | cancelPaging() 146 | } 147 | } 148 | } 149 | 150 | func Example_client_EstimateTime() { 151 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 152 | if err != nil { 153 | log.Fatal(err) 154 | } 155 | 156 | estimatesPageChan, cancelPaging, err := client.EstimateTime(&uber.EstimateRequest{ 157 | StartLatitude: 37.7752315, 158 | EndLatitude: 37.7752415, 159 | StartLongitude: -122.418075, 160 | EndLongitude: -122.518075, 161 | 162 | // Comment out to search only for estimates for: uberXL 163 | // ProductID: "821415d8-3bd5-4e27-9604-194e4359a449", 164 | }) 165 | 166 | if err != nil { 167 | log.Fatal(err) 168 | } 169 | 170 | itemCount := uint64(0) 171 | for page := range estimatesPageChan { 172 | if page.Err != nil { 173 | fmt.Printf("PageNumber: #%d err: %v", page.PageNumber, page.Err) 174 | continue 175 | } 176 | 177 | for i, estimate := range page.Estimates { 178 | itemCount += 1 179 | fmt.Printf("Estimate: #%d ==> %#v\n", i, estimate) 180 | } 181 | 182 | if itemCount >= 23 { 183 | cancelPaging() 184 | } 185 | } 186 | } 187 | 188 | func Example_client_RetrieveMyProfile() { 189 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 190 | if err != nil { 191 | log.Fatal(err) 192 | } 193 | 194 | myProfile, err := client.RetrieveMyProfile() 195 | if err != nil { 196 | log.Fatal(err) 197 | } 198 | 199 | fmt.Printf("Here is my profile: %#v\n", myProfile) 200 | } 201 | 202 | func Example_client_ApplyPromoCode() { 203 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 204 | if err != nil { 205 | log.Fatal(err) 206 | } 207 | 208 | appliedPromoCode, err := client.ApplyPromoCode("uberd340ue") 209 | if err != nil { 210 | log.Fatal(err) 211 | } 212 | 213 | fmt.Printf("AppliedPromoCode: %#v\n", appliedPromoCode) 214 | } 215 | 216 | func Example_client_RequestReceipt() { 217 | client, err := uber.NewClient() 218 | if err != nil { 219 | log.Fatal(err) 220 | } 221 | 222 | receipt, err := client.RequestReceipt("b5512127-a134-4bf4-b1ba-fe9f48f56d9d") 223 | if err != nil { 224 | log.Fatal(err) 225 | } 226 | 227 | fmt.Printf("That receipt: %#v\n", receipt) 228 | } 229 | 230 | func Example_client_RetrieveHomeAddress() { 231 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 232 | if err != nil { 233 | log.Fatal(err) 234 | } 235 | 236 | place, err := client.Place(uber.PlaceHome) 237 | if err != nil { 238 | log.Fatal(err) 239 | } 240 | 241 | fmt.Printf("My home address: %#v\n", place.Address) 242 | } 243 | 244 | func Example_client_RetrieveWorkAddress() { 245 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 246 | if err != nil { 247 | log.Fatal(err) 248 | } 249 | 250 | place, err := client.Place(uber.PlaceWork) 251 | if err != nil { 252 | log.Fatal(err) 253 | } 254 | 255 | fmt.Printf("My work address: %#v\n", place.Address) 256 | } 257 | 258 | func Example_client_UpdateHomeAddress() { 259 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 260 | if err != nil { 261 | log.Fatal(err) 262 | } 263 | 264 | updatedHome, err := client.UpdatePlace(&uber.PlaceParams{ 265 | Place: uber.PlaceHome, 266 | Address: "685 Market St, San Francisco, CA 94103, USA", 267 | }) 268 | if err != nil { 269 | log.Fatal(err) 270 | } 271 | 272 | fmt.Printf("My updated home address: %#v\n", updatedHome) 273 | } 274 | 275 | func Example_client_UpdateWorkAddress() { 276 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 277 | if err != nil { 278 | log.Fatal(err) 279 | } 280 | 281 | updatedWork, err := client.UpdatePlace(&uber.PlaceParams{ 282 | Place: uber.PlaceWork, 283 | Address: "685 Market St, San Francisco, CA 94103, USA", 284 | }) 285 | if err != nil { 286 | log.Fatal(err) 287 | } 288 | 289 | fmt.Printf("My updated work address: %#v\n", updatedWork) 290 | } 291 | 292 | func Example_client_RequestMap() { 293 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 294 | if err != nil { 295 | log.Fatal(err) 296 | } 297 | 298 | tripMapInfo, err := client.RequestMap("b5512127-a134-4bf4-b1ba-fe9f48f56d9d") 299 | if err != nil { 300 | log.Fatal(err) 301 | } 302 | 303 | fmt.Printf("Visit the URL: %q for more information\n", tripMapInfo.URL) 304 | } 305 | 306 | func Example_client_OpenMap() { 307 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 308 | if err != nil { 309 | log.Fatal(err) 310 | } 311 | 312 | if err := client.OpenMapForTrip("6468d5f6-c7fc-4a77-8cbc-f0b7c79de1a7"); err != nil { 313 | log.Fatal(err) 314 | } 315 | } 316 | 317 | func Example_client_UpfrontFare() { 318 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 319 | if err != nil { 320 | log.Fatal(err) 321 | } 322 | 323 | upfrontFare, err := client.UpfrontFare(&uber.EstimateRequest{ 324 | StartLatitude: 37.7752315, 325 | EndLatitude: 37.7752415, 326 | StartLongitude: -122.418075, 327 | EndLongitude: -122.518075, 328 | SeatCount: 2, 329 | }) 330 | if err != nil { 331 | log.Fatal(err) 332 | } 333 | 334 | if upfrontFare.SurgeInEffect() { 335 | fmt.Printf("Surge is in effect!\n") 336 | fmt.Printf("Please visit this URL to confirm %q then"+ 337 | "request again", upfrontFare.Estimate.SurgeConfirmationURL) 338 | return 339 | } 340 | 341 | fmt.Printf("Fare: %#v\n", upfrontFare.Fare) 342 | fmt.Printf("Trip: %#v\n", upfrontFare.Trip) 343 | } 344 | 345 | func Example_client_RequestRide() { 346 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 347 | if err != nil { 348 | log.Fatal(err) 349 | } 350 | 351 | ride, err := client.RequestRide(&uber.RideRequest{ 352 | StartLatitude: 37.7752315, 353 | StartLongitude: -122.418075, 354 | EndLatitude: 37.7752415, 355 | EndLongitude: -122.518075, 356 | PromptOnFare: func(fare *uber.UpfrontFare) error { 357 | if fare.Fare.Value >= 6.00 { 358 | return fmt.Errorf("exercise can't hurt instead of $6.00 for that walk!") 359 | } 360 | return nil 361 | }, 362 | }) 363 | if err != nil { 364 | log.Fatalf("ride request err: %v", err) 365 | } 366 | 367 | fmt.Printf("Your ride information: %+v\n", ride) 368 | } 369 | 370 | func Example_client_RequestDelivery() { 371 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 372 | if err != nil { 373 | log.Fatal(err) 374 | } 375 | 376 | deliveryConfirmation, err := client.RequestDelivery(&uber.DeliveryRequest{ 377 | Pickup: &uber.Endpoint{ 378 | Contact: &uber.Contact{ 379 | CompanyName: "orijtech", 380 | Email: "deliveries@orijtech.com", 381 | SendSMSNotifications: true, 382 | }, 383 | Location: &uber.Location{ 384 | PrimaryAddress: "Empire State Building", 385 | State: "NY", 386 | Country: "US", 387 | }, 388 | SpecialInstructions: "Please ask guest services for \"I Man\"", 389 | }, 390 | Dropoff: &uber.Endpoint{ 391 | Contact: &uber.Contact{ 392 | FirstName: "delivery", 393 | LastName: "bot", 394 | CompanyName: "Uber", 395 | 396 | SendEmailNotifications: true, 397 | }, 398 | Location: &uber.Location{ 399 | PrimaryAddress: "530 W 113th Street", 400 | SecondaryAddress: "Floor 2", 401 | Country: "US", 402 | PostalCode: "10025", 403 | State: "NY", 404 | }, 405 | }, 406 | Items: []*uber.Item{ 407 | { 408 | Title: "phone chargers", 409 | Quantity: 10, 410 | }, 411 | { 412 | Title: "Blue prints", 413 | Fragile: true, 414 | Quantity: 1, 415 | }, 416 | }, 417 | }) 418 | if err != nil { 419 | log.Fatal(err) 420 | } 421 | 422 | log.Printf("The confirmation: %+v\n", deliveryConfirmation) 423 | } 424 | 425 | func Example_client_CancelDelivery() { 426 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 427 | if err != nil { 428 | log.Fatal(err) 429 | } 430 | 431 | err = client.CancelDelivery("71a969ca-5359-4334-a7b7-5a1705869c51") 432 | if err == nil { 433 | log.Printf("Successfully canceled that delivery!") 434 | } else { 435 | log.Printf("Failed to cancel that delivery, err: %v", err) 436 | } 437 | } 438 | 439 | func Example_client_ListProducts() { 440 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 441 | if err != nil { 442 | log.Fatal(err) 443 | } 444 | 445 | products, err := client.ListProducts(&uber.Place{ 446 | Latitude: 38.8971, 447 | Longitude: -77.0366, 448 | }) 449 | if err != nil { 450 | log.Fatal(err) 451 | } 452 | 453 | for i, product := range products { 454 | fmt.Printf("#%d: ID: %q Product: %#v\n", i, product.ID, product) 455 | } 456 | } 457 | 458 | func Example_client_ProductByID() { 459 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 460 | if err != nil { 461 | log.Fatal(err) 462 | } 463 | 464 | product, err := client.ProductByID("bc300c14-c30d-4d3f-afcb-19b240c16a13") 465 | if err != nil { 466 | log.Fatal(err) 467 | } 468 | 469 | fmt.Printf("The Product information: %#v\n", product) 470 | } 471 | 472 | func Example_client_ListDeliveries() { 473 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 474 | if err != nil { 475 | log.Fatal(err) 476 | } 477 | 478 | delivRes, err := client.ListDeliveries(&uber.DeliveryListRequest{ 479 | Status: uber.StatusCompleted, 480 | StartOffset: 20, 481 | }) 482 | if err != nil { 483 | log.Fatal(err) 484 | } 485 | 486 | itemCount := uint64(0) 487 | for page := range delivRes.Pages { 488 | if page.Err != nil { 489 | fmt.Printf("Page #%d err: %v", page.PageNumber, page.Err) 490 | } 491 | for i, delivery := range page.Deliveries { 492 | fmt.Printf("\t(%d): %#v\n", i, delivery) 493 | itemCount += 1 494 | } 495 | if itemCount >= 10 { 496 | delivRes.Cancel() 497 | } 498 | } 499 | } 500 | 501 | func Example_client_DriverProfile() { 502 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 503 | if err != nil { 504 | log.Fatal(err) 505 | } 506 | prof, err := client.DriverProfile() 507 | if err != nil { 508 | log.Fatal(err) 509 | } 510 | fmt.Printf("Rating: %.2f\nFirst Name: %s\nLast Name: %s\n", 511 | prof.Rating, prof.FirstName, prof.LastName) 512 | } 513 | 514 | func Example_client_ListDriverPayments() { 515 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 516 | if err != nil { 517 | log.Fatal(err) 518 | } 519 | aWeekAgo := time.Now().Add(-1 * time.Hour * 7 * 24) 520 | yesterday := time.Now().Add(-1 * time.Hour * 24) 521 | payments, err := client.ListDriverPayments(&uber.DriverInfoQuery{ 522 | StartDate: &aWeekAgo, 523 | EndDate: &yesterday, 524 | }) 525 | if err != nil { 526 | log.Fatal(err) 527 | } 528 | for page := range payments.Pages { 529 | if page.Err != nil { 530 | fmt.Printf("%d err: %v\n", page.PageNumber, page.Err) 531 | continue 532 | } 533 | for i, payment := range page.Payments { 534 | fmt.Printf("\t%d:: %#v\n", i, payment) 535 | } 536 | fmt.Println() 537 | } 538 | } 539 | 540 | func Example_client_ListDriverTrips() { 541 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 542 | if err != nil { 543 | log.Fatal(err) 544 | } 545 | aWeekAgo := time.Now().Add(-1 * time.Hour * 7 * 24) 546 | yesterday := time.Now().Add(-1 * time.Hour * 24) 547 | trips, err := client.ListDriverTrips(&uber.DriverInfoQuery{ 548 | StartDate: &aWeekAgo, 549 | EndDate: &yesterday, 550 | MaxPageNumber: 2, 551 | Offset: 4, 552 | }) 553 | if err != nil { 554 | log.Fatal(err) 555 | } 556 | for page := range trips.Pages { 557 | if page.Err != nil { 558 | fmt.Printf("%d err: %v\n", page.PageNumber, page.Err) 559 | continue 560 | } 561 | for i, trip := range page.Trips { 562 | fmt.Printf("\t%d:: DriverID: %q\nFare: %.2f\n%#v\n", i, trip.DriverID, trip.Fare, trip) 563 | } 564 | fmt.Println() 565 | } 566 | } 567 | 568 | func Example_client_TripByID() { 569 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 570 | if err != nil { 571 | log.Fatal(err) 572 | } 573 | trip, err := client.TripByID("6468d5f6-c7fc-4a77-8cbc-f0b7c79de1a7") 574 | if err != nil { 575 | log.Fatal(err) 576 | } 577 | fmt.Printf("ProductID: %s\n", trip.ProductID) 578 | fmt.Printf("RequestID: %s\n\n", trip.RequestID) 579 | fmt.Printf("Destination: %#v\nPickup: %#v\nDropoff: %#v\n", 580 | trip.Destination, trip.Pickup, trip.Dropoff) 581 | 582 | if len(trip.Waypoints) > 0 { 583 | fmt.Printf("You have %d Waypoints\n", len(trip.Waypoints)) 584 | for i, waypoint := range trip.Waypoints { 585 | fmt.Printf("\tWaypoint #%d: %v\n", i, waypoint) 586 | } 587 | fmt.Println() 588 | } 589 | if len(trip.Riders) > 0 { 590 | fmt.Printf("You have %d co-Riders\n", len(trip.Riders)) 591 | for i, rider := range trip.Riders { 592 | fmt.Printf("\tRider #%d: %v\n", i, rider) 593 | } 594 | } 595 | } 596 | 597 | func Example_client_CurrentTrip() { 598 | client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) 599 | if err != nil { 600 | log.Fatal(err) 601 | } 602 | trip, err := client.CurrentTrip() 603 | if err != nil { 604 | log.Fatal(err) 605 | } 606 | fmt.Printf("ProductID: %s\n", trip.ProductID) 607 | fmt.Printf("RequestID: %s\n\n", trip.RequestID) 608 | fmt.Printf("Destination: %#v\nPickup: %#v\nDropoff: %#v\n", 609 | trip.Destination, trip.Pickup, trip.Dropoff) 610 | 611 | if len(trip.Waypoints) > 0 { 612 | fmt.Printf("You have %d Waypoints\n", len(trip.Waypoints)) 613 | for i, waypoint := range trip.Waypoints { 614 | fmt.Printf("\tWaypoint #%d: %v\n", i, waypoint) 615 | } 616 | fmt.Println() 617 | } 618 | if len(trip.Riders) > 0 { 619 | fmt.Printf("You have %d co-Riders\n", len(trip.Riders)) 620 | for i, rider := range trip.Riders { 621 | fmt.Printf("\tRider #%d: %v\n", i, rider) 622 | } 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oauth2 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "io/ioutil" 23 | "log" 24 | "math/rand" 25 | "net/http" 26 | "net/url" 27 | "os" 28 | "reflect" 29 | "strings" 30 | "sync" 31 | "time" 32 | 33 | "golang.org/x/oauth2" 34 | ) 35 | 36 | type OAuth2AppConfig struct { 37 | ClientID string `json:"client_id"` 38 | ClientSecret string `json:"client_secret"` 39 | RedirectURL string `json:"redirect_url"` 40 | } 41 | 42 | var ( 43 | envOAuth2ClientIDKey = "UBER_APP_OAUTH2_CLIENT_ID" 44 | envOAuth2ClientSecretKey = "UBER_APP_OAUTH2_CLIENT_SECRET" 45 | ) 46 | 47 | func Transport(token *oauth2.Token) *oauth2.Transport { 48 | // Once we have the token we can now make the TokenSource 49 | ts := &tokenSourcer{token: token} 50 | return &oauth2.Transport{Source: ts} 51 | } 52 | 53 | func TransportWithBase(token *oauth2.Token, base http.RoundTripper) *oauth2.Transport { 54 | tr := Transport(token) 55 | tr.Base = base 56 | return tr 57 | } 58 | 59 | var ( 60 | errNoOAuth2TokenDeserialized = errors.New("unable to deserialize an OAuth2.0 token") 61 | 62 | blankOAuth2Token = oauth2.Token{} 63 | ) 64 | 65 | func TransportFromFile(path string) (*oauth2.Transport, error) { 66 | blob, err := ioutil.ReadFile(path) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | token := new(oauth2.Token) 72 | if err := json.Unmarshal(blob, token); err != nil { 73 | return nil, err 74 | } 75 | if reflect.DeepEqual(blankOAuth2Token, *token) { 76 | return nil, errNoOAuth2TokenDeserialized 77 | } 78 | 79 | return Transport(token), nil 80 | } 81 | 82 | type tokenSourcer struct { 83 | sync.RWMutex 84 | token *oauth2.Token 85 | } 86 | 87 | var _ oauth2.TokenSource = (*tokenSourcer)(nil) 88 | 89 | func (ts *tokenSourcer) Token() (*oauth2.Token, error) { 90 | ts.RLock() 91 | defer ts.RUnlock() 92 | 93 | return ts.token, nil 94 | } 95 | 96 | const ( 97 | OAuth2AuthURL = "https://login.uber.com/oauth/v2/authorize" 98 | OAuth2TokenURL = "https://login.uber.com/oauth/v2/token" 99 | ) 100 | 101 | // OAuth2ConfigFromEnv retrieves your app's client id and client 102 | // secret from your environment, with the purpose of later being 103 | // able to perform application functions on behalf of users. 104 | func OAuth2ConfigFromEnv() (*OAuth2AppConfig, error) { 105 | var errsList []string 106 | oauth2ClientID := strings.TrimSpace(os.Getenv(envOAuth2ClientIDKey)) 107 | if oauth2ClientID == "" { 108 | errsList = append(errsList, fmt.Sprintf("%q was not set", envOAuth2ClientIDKey)) 109 | } 110 | oauth2ClientSecret := strings.TrimSpace(os.Getenv(envOAuth2ClientSecretKey)) 111 | if oauth2ClientSecret == "" { 112 | errsList = append(errsList, fmt.Sprintf("%q was not set", envOAuth2ClientSecretKey)) 113 | } 114 | 115 | if len(errsList) > 0 { 116 | return nil, errors.New(strings.Join(errsList, "\n")) 117 | } 118 | 119 | config := &OAuth2AppConfig{ 120 | ClientID: oauth2ClientID, 121 | ClientSecret: oauth2ClientSecret, 122 | } 123 | 124 | return config, nil 125 | } 126 | 127 | const ( 128 | // Access the user's basic profile information 129 | // on a user's Uber account including their 130 | // firstname, email address and profile picture. 131 | ScopeProfile = "profile" 132 | 133 | // Pull trip data including times, product 134 | // type andd city information of a user's 135 | // historical pickups and drop-offs. 136 | ScopeHistory = "history" 137 | 138 | // ScopeHistoryLite is the same as 139 | // ScopeHistory but without city information. 140 | ScopeHistoryLite = "history_lite" 141 | 142 | // Access to get and update your saved places. 143 | // This includes your home and work addresses if 144 | // you have saved them with Uber. 145 | ScopePlaces = "places" 146 | 147 | // Allows developers to provide a complete 148 | // Uber ride experience inside their app 149 | // using the widget. Enables users to access 150 | // trip information for rides requested through 151 | // the app and the current ride, available promos, 152 | // and payment methods (last two digits only) 153 | // using the widget. Uber's charges, terms 154 | // and policies will apply. 155 | ScopeRideWidgets = "ride_widgets" 156 | 157 | // ScopeRequest is a privileged scope that 158 | // allows your application to make requests 159 | // for Uber products on behalf of users. 160 | ScopeRequest = "request" 161 | 162 | // ScopeRequestReceipt is a privileged scope that 163 | // allows your application to get receipt details 164 | // for requests made by the application. 165 | // Restrictions: This scope is only granted to apps 166 | // that request Uber rides directly and receipts 167 | // as part of the trip lifecycle. We do not allow 168 | // apps to aggregate receipt information. The receipt 169 | // endpoint will only provide receipts for ride requests 170 | // origination from your application. It is not 171 | // currently possible to receive receipt 172 | // data for all trips, as of: (Fri 12 May 2017 18:18:42 MDT). 173 | ScopeRequestReceipt = "request_receipt" 174 | 175 | // ScopeAllTrips is a privileged scope that allows 176 | // access to trip details about all future Uber trips, 177 | // including pickup, destination and real-time 178 | // location for all of your future rides. 179 | ScopeAllTrips = "all_trips" 180 | 181 | // ScopeDelivery is a privileged scope that gives 182 | // access to the authenticated user's deliveries. 183 | ScopeDelivery = "delivery" 184 | 185 | // ScopePartnerAccounts is a privileged scope that 186 | // means that you've authorized Uber to share your 187 | // account information with this partner. 188 | ScopePartnerAccounts = "partner.accounts" 189 | 190 | // ScopePartnerPayments is a privileged scope that 191 | // authorizes Uber to share your payments information 192 | // with this partner. 193 | ScopePartnerPayments = "partner.payments" 194 | 195 | // ScopePartnerTrips is a privileged scope that 196 | // authorizes Uber to share your trip times, fares, 197 | // distances and cities with this partner. 198 | ScopePartnerTrips = "partner.trips" 199 | ) 200 | 201 | func AuthorizeByEnvApp(scopes ...string) (*oauth2.Token, error) { 202 | oconfig, err := OAuth2ConfigFromEnv() 203 | if err != nil { 204 | return nil, err 205 | } 206 | return Authorize(oconfig, scopes...) 207 | } 208 | 209 | func Authorize(oconfig *OAuth2AppConfig, scopes ...string) (*oauth2.Token, error) { 210 | config := &oauth2.Config{ 211 | ClientID: oconfig.ClientID, 212 | ClientSecret: oconfig.ClientSecret, 213 | Scopes: scopes, 214 | Endpoint: oauth2.Endpoint{ 215 | AuthURL: OAuth2AuthURL, 216 | TokenURL: OAuth2TokenURL, 217 | }, 218 | RedirectURL: oconfig.RedirectURL, 219 | } 220 | 221 | srvAddr := ":8889" 222 | if config.RedirectURL == "" { 223 | config.RedirectURL = fmt.Sprintf("http://localhost%s/", srvAddr) 224 | } 225 | 226 | state := fmt.Sprintf("%v%f", time.Now().Unix(), rand.Float32()) 227 | urlToVisit := config.AuthCodeURL(state, oauth2.AccessTypeOffline) 228 | fmt.Printf("Please visit this URL for the auth dialog: %v\n", urlToVisit) 229 | 230 | callbackURLChan := make(chan url.Values) 231 | go func() { 232 | http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { 233 | query := req.URL.Query() 234 | callbackURLChan <- query 235 | fmt.Fprintf(rw, "Received the token successfully. Please return to your terminal") 236 | }) 237 | 238 | defer close(callbackURLChan) 239 | if err := http.ListenAndServe(srvAddr, nil); err != nil { 240 | log.Fatal(err) 241 | } 242 | }() 243 | 244 | urlValues := <-callbackURLChan 245 | gotState, wantState := urlValues.Get("state"), state 246 | if gotState != wantState { 247 | return nil, fmt.Errorf("states do not match: got: %q want: %q", gotState, wantState) 248 | } 249 | code := urlValues.Get("code") 250 | 251 | ctx := context.Background() 252 | token, err := config.Exchange(ctx, code) 253 | if err != nil { 254 | return nil, err 255 | } 256 | return token, nil 257 | } 258 | -------------------------------------------------------------------------------- /uberhook/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uberhook_test 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "net/http" 21 | 22 | "golang.org/x/crypto/acme/autocert" 23 | 24 | "github.com/orijtech/otils" 25 | "github.com/orijtech/uber/uberhook" 26 | ) 27 | 28 | func Example_Server() { 29 | webhook, err := uberhook.New() 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | mux := http.NewServeMux() 35 | mux.Handle("/", webhook.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 | defer r.Body.Close() 37 | event, err := uberhook.FparseEvent(r.Body) 38 | 39 | if err != nil { 40 | http.Error(w, err.Error(), http.StatusBadRequest) 41 | return 42 | } 43 | log.Printf("Got an event from Uber: %#v\n", event) 44 | fmt.Fprintf(w, "Successfully retrieved event!\n") 45 | }))) 46 | 47 | go func() { 48 | nonHTTPSHandler := otils.RedirectAllTrafficTo("https://uberhook.example.com") 49 | if err := http.ListenAndServe(":80", nonHTTPSHandler); err != nil { 50 | log.Fatal(err) 51 | } 52 | }() 53 | 54 | domains := []string{ 55 | "uberhook.example.com", 56 | "www.uberhook.example.com", 57 | } 58 | 59 | log.Fatal(http.Serve(autocert.NewListener(domains...), mux)) 60 | } 61 | -------------------------------------------------------------------------------- /uberhook/uberhook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uberhook 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "io" 21 | "io/ioutil" 22 | "net/http" 23 | "sync" 24 | 25 | "github.com/orijtech/authmid" 26 | "github.com/orijtech/uber/oauth2" 27 | "github.com/orijtech/uber/v1" 28 | ) 29 | 30 | type Event struct { 31 | ID string `json:"event_id"` 32 | TimeUnix int64 `json:"event_time"` 33 | Type string `json:"event_type"` 34 | 35 | Meta *Meta `json:"meta"` 36 | 37 | URL string `json:"resource_href"` 38 | } 39 | 40 | type Status string 41 | 42 | type Meta struct { 43 | UserID string `json:"user_id"` 44 | ResourceID string `json:"resource_id"` 45 | Status uber.Status `json:"status"` 46 | } 47 | 48 | type Webhook struct { 49 | sync.RWMutex 50 | oauthConfig *oauth2.OAuth2AppConfig 51 | } 52 | 53 | var _ authmid.Authenticator = (*Webhook)(nil) 54 | 55 | var ( 56 | errBlankClientID = errors.New("expecting a non-blank clientID") 57 | errBlankClientSecret = errors.New("expecting a non-blank clientSecret") 58 | ) 59 | 60 | func (v *Webhook) HeaderValues(hdr http.Header) ([]string, []string, error) { 61 | return nil, nil, nil 62 | } 63 | 64 | func (v *Webhook) LookupAPIKey(hdr http.Header) (string, error) { 65 | // Uber doesn't include the APIKey as part of the header signatures 66 | // at least as of `Sat 10 Jun 2017 01:20:15 MDT` so send back a blank. 67 | // Reference: https://developer.uber.com/docs/riders/guides/webhooks 68 | // has no mention of client_id in there, only client_secret 69 | return "", nil 70 | } 71 | 72 | func (v *Webhook) LookupSecret(apiKey string) ([]byte, error) { 73 | var err error = errBlankClientSecret 74 | var secret []byte 75 | if v != nil { 76 | v.RLock() 77 | if v.oauthConfig != nil { 78 | secret = []byte(v.oauthConfig.ClientSecret) 79 | err = nil 80 | } 81 | v.RUnlock() 82 | } 83 | return secret, err 84 | } 85 | 86 | func (v *Webhook) Signature(hdr http.Header) (string, error) { 87 | return hdr.Get("X-Uber-Signature"), nil 88 | } 89 | 90 | func New() (*Webhook, error) { 91 | oauth2Config, err := oauth2.OAuth2ConfigFromEnv() 92 | if err != nil { 93 | return nil, err 94 | } 95 | return &Webhook{oauthConfig: oauth2Config}, nil 96 | } 97 | 98 | func (w *Webhook) Middleware(next http.Handler) http.Handler { 99 | return authmid.Middleware(w, next) 100 | } 101 | 102 | var _ authmid.ExcludeMethodAndPather = (*Webhook)(nil) 103 | 104 | // Uber's webhook signature verification only consists of (clientSecret, webhookBody) 105 | func (w *Webhook) ExcludeMethodAndPath() bool { return true } 106 | 107 | var blankEvent Event 108 | var ( 109 | errBlankEvent = errors.New("expecting a non-blank event") 110 | ) 111 | 112 | func FparseEvent(r io.Reader) (*Event, error) { 113 | blob, err := ioutil.ReadAll(r) 114 | if err != nil { 115 | return nil, err 116 | } 117 | ev := new(Event) 118 | if err := json.Unmarshal(blob, ev); err != nil { 119 | return nil, err 120 | } 121 | if *ev == blankEvent { 122 | return nil, errBlankEvent 123 | } 124 | return ev, nil 125 | } 126 | -------------------------------------------------------------------------------- /v1/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io/ioutil" 21 | "net/http" 22 | "os" 23 | "reflect" 24 | "strings" 25 | "sync" 26 | 27 | "golang.org/x/oauth2" 28 | 29 | "github.com/orijtech/otils" 30 | uberOAuth2 "github.com/orijtech/uber/oauth2" 31 | ) 32 | 33 | const envUberTokenKey = "UBER_TOKEN_KEY" 34 | 35 | var errUnsetTokenEnvKey = fmt.Errorf("could not find %q in your environment", envUberTokenKey) 36 | 37 | type Client struct { 38 | sync.RWMutex 39 | 40 | rt http.RoundTripper 41 | token string 42 | sandboxed bool 43 | } 44 | 45 | func (c *Client) hasServerToken() bool { 46 | c.RLock() 47 | defer c.RUnlock() 48 | 49 | return c.token != "" 50 | } 51 | 52 | // Sandboxed if set to true, the client will send requests 53 | // to the sandboxed API endpoint. 54 | // See: 55 | // + https://developer.uber.com/docs/riders/guides/sandbox 56 | // + https://developer.uber.com/docs/drivers 57 | func (c *Client) SetSandboxMode(sandboxed bool) { 58 | c.Lock() 59 | c.sandboxed = sandboxed 60 | c.Unlock() 61 | } 62 | 63 | func (c *Client) Sandboxed() bool { 64 | c.RLock() 65 | defer c.RUnlock() 66 | 67 | return c.sandboxed 68 | } 69 | 70 | const defaultVersion = "v1.2" 71 | 72 | func (c *Client) baseURL(versions ...string) string { 73 | // Setting the baseURLs in here to ensure that no-one mistakenly 74 | // directly invokes baseURL or sandboxBaseURL. 75 | c.RLock() 76 | defer c.RUnlock() 77 | 78 | version := otils.FirstNonEmptyString(versions...) 79 | if version == "" { 80 | version = defaultVersion 81 | } 82 | 83 | if c.sandboxed { 84 | return "https://sandbox-api.uber.com/" + version 85 | } else { // Invoking the production endpoint 86 | return "https://api.uber.com/" + version 87 | } 88 | } 89 | 90 | // Some endpoints require us to hit /v1 instead of /v1.2 as in Client.baseURL. 91 | // These endpoints include: 92 | // + ListDeliveries --> /v1/deliveries at least as of "Tue 4 Jul 2017 23:17:14 MDT" 93 | func (c *Client) legacyV1BaseURL() string { 94 | // Setting the baseURLs in here to ensure that no-one mistakenly 95 | // directly invokes baseURL or sandboxBaseURL. 96 | c.RLock() 97 | defer c.RUnlock() 98 | 99 | if c.sandboxed { 100 | return "https://sandbox-api.uber.com/v1" 101 | } else { // Invoking the production endpoint 102 | return "https://api.uber.com/v1" 103 | } 104 | } 105 | 106 | func NewClient(tokens ...string) (*Client, error) { 107 | if token := otils.FirstNonEmptyString(tokens...); token != "" { 108 | return &Client{token: token}, nil 109 | } 110 | 111 | // Otherwise fallback to retrieving it from the environment 112 | return NewClientFromEnv() 113 | } 114 | 115 | func NewSandboxedClient(tokens ...string) (*Client, error) { 116 | c, err := NewClient(tokens...) 117 | if err == nil && c != nil { 118 | c.SetSandboxMode(true) 119 | } 120 | return c, err 121 | } 122 | 123 | func NewSandboxedClientFromEnv() (*Client, error) { 124 | c, err := NewClientFromEnv() 125 | if err == nil && c != nil { 126 | c.SetSandboxMode(true) 127 | } 128 | return c, err 129 | } 130 | 131 | func NewClientFromEnv() (*Client, error) { 132 | retrToken := strings.TrimSpace(os.Getenv(envUberTokenKey)) 133 | if retrToken == "" { 134 | return nil, errUnsetTokenEnvKey 135 | } 136 | 137 | return &Client{token: retrToken}, nil 138 | } 139 | 140 | func (c *Client) SetHTTPRoundTripper(rt http.RoundTripper) { 141 | c.Lock() 142 | c.rt = rt 143 | c.Unlock() 144 | } 145 | 146 | func (c *Client) SetBearerToken(token string) { 147 | c.Lock() 148 | defer c.Unlock() 149 | 150 | c.token = token 151 | } 152 | 153 | func (c *Client) httpClient() *http.Client { 154 | c.RLock() 155 | rt := c.rt 156 | c.RUnlock() 157 | 158 | if rt == nil { 159 | rt = http.DefaultTransport 160 | } 161 | 162 | return &http.Client{Transport: rt} 163 | } 164 | 165 | func (c *Client) bearerToken() string { 166 | c.RLock() 167 | defer c.RUnlock() 168 | 169 | return fmt.Sprintf("Bearer %s", c.token) 170 | } 171 | 172 | func (c *Client) tokenToken() string { 173 | c.RLock() 174 | defer c.RUnlock() 175 | 176 | return fmt.Sprintf("Token %s", c.token) 177 | } 178 | 179 | func (c *Client) doAuthAndHTTPReq(req *http.Request) ([]byte, http.Header, error) { 180 | req.Header.Set("Authorization", c.bearerToken()) 181 | return c.doHTTPReq(req) 182 | } 183 | 184 | func (c *Client) doReq(req *http.Request) ([]byte, http.Header, error) { 185 | if c.hasServerToken() { 186 | req.Header.Set("Authorization", c.bearerToken()) 187 | } 188 | return c.doHTTPReq(req) 189 | } 190 | 191 | func (c *Client) doHTTPReq(req *http.Request) ([]byte, http.Header, error) { 192 | res, err := c.httpClient().Do(req) 193 | if err != nil { 194 | return nil, nil, err 195 | } 196 | if res.Body != nil { 197 | defer res.Body.Close() 198 | } 199 | 200 | if !otils.StatusOK(res.StatusCode) { 201 | errMsg := res.Status 202 | var err error 203 | if res.Body != nil { 204 | slurp, _ := ioutil.ReadAll(res.Body) 205 | if len(slurp) > 3 { 206 | ue := new(Error) 207 | plainUE := new(Error) 208 | if jerr := json.Unmarshal(slurp, ue); jerr == nil && !reflect.DeepEqual(ue, plainUE) { 209 | err = ue 210 | } else { 211 | errMsg = string(slurp) 212 | } 213 | } 214 | } 215 | if err == nil { 216 | err = otils.MakeCodedError(errMsg, res.StatusCode) 217 | } 218 | return nil, res.Header, err 219 | } 220 | 221 | blob, err := ioutil.ReadAll(res.Body) 222 | return blob, res.Header, err 223 | } 224 | 225 | func NewClientFromOAuth2Token(token *oauth2.Token) (*Client, error) { 226 | // Once we have the token we can now make the TokenSource 227 | oauth2Transport := uberOAuth2.Transport(token) 228 | return &Client{rt: oauth2Transport}, nil 229 | } 230 | 231 | func NewClientFromOAuth2File(tokenFilepath string) (*Client, error) { 232 | oauth2Transport, err := uberOAuth2.TransportFromFile(tokenFilepath) 233 | if err != nil { 234 | return nil, err 235 | } 236 | return &Client{rt: oauth2Transport}, nil 237 | } 238 | -------------------------------------------------------------------------------- /v1/deliveries.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "net/http" 23 | "net/url" 24 | "strings" 25 | "time" 26 | 27 | "github.com/orijtech/otils" 28 | ) 29 | 30 | type DeliveryRequest struct { 31 | // The ID of the quoted price of the 32 | // delivery. This field is optional. 33 | // If missing, the fee for the delivery will 34 | // be determined at the time of the request. 35 | QuoteID string `json:"quote_id,omitempty"` 36 | 37 | // The merchant supplied order reference identifier. 38 | // This field is optional and it is limited to 256 characters. 39 | OrderReferenceID string `json:"order_reference_id,omitempty"` 40 | 41 | // The items being delivered. 42 | Items []*Item `json:"items"` 43 | 44 | // The details of the delivery pickup. 45 | Pickup *Endpoint `json:"pickup"` 46 | Dropoff *Endpoint `json:"dropoff"` 47 | } 48 | 49 | type Item struct { 50 | Title string `json:"title"` 51 | Fragile bool `json:"is_fragile,omitempty"` 52 | Quantity int `json:"quantity"` 53 | 54 | WidthInches float32 `json:"width,omitempty"` 55 | HeightInches float32 `json:"height,omitempty"` 56 | LengthInches float32 `json:"length,omitempty"` 57 | 58 | CurrencyCode CurrencyCode `json:"currency_code,omitempty"` 59 | } 60 | 61 | type Endpoint struct { 62 | Location *Location `json:"location,omitempty"` 63 | Contact *Contact `json:"contact,omitempty"` 64 | 65 | // Special instructions for the endpoint. This field 66 | // is optional and it is limited to 256 characters. 67 | SpecialInstructions otils.NullableString `json:"special_instructions,omitempty"` 68 | 69 | SignatureRequired bool `json:"signature_required,omitempty"` 70 | 71 | // Indicates if the delivery includes alcohol. This 72 | // feature is only available to whitelisted businesses. 73 | IncludesAlcohol bool `json:"includes_alcohol,omitempty"` 74 | 75 | ETAMinutes int `json:"eta,omitempty"` 76 | 77 | TimestampUnix int64 `json:"timestamp,omitempty"` 78 | } 79 | 80 | type Contact struct { 81 | FirstName string `json:"first_name,omitempty"` 82 | LastName string `json:"last_name,omitempty"` 83 | CompanyName string `json:"company_name,omitempty"` 84 | Email string `json:"email,omitempty"` 85 | Phone *Phone `json:"phone,omitempty"` 86 | 87 | // SendEmailNotifications if set requests that 88 | // Uber send email delivery notifications. 89 | // This field is optional and defaults to true. 90 | SendEmailNotifications bool `json:"send_email_notifications,omitempty"` 91 | 92 | // SendSMSNotifications if set requests that 93 | // Uber send SMS delivery notifications. 94 | // This field is optional and defaults to true. 95 | SendSMSNotifications bool `json:"send_sms_notifications,omitempty"` 96 | } 97 | 98 | type Phone struct { 99 | Number string `json:"number"` 100 | SMSEnabled bool `json:"sms_enabled"` 101 | } 102 | 103 | type CurrencyCode string 104 | 105 | type Delivery struct { 106 | ID string `json:"delivery_id"` 107 | Fee float32 `json:"fee"` 108 | QuoteID string `json:"quote_id"` 109 | Status Status `json:"status"` 110 | 111 | Courier *Contact `json:"courier,omitempty"` 112 | 113 | OrderReferenceID string `json:"order_reference_id"` 114 | 115 | CurrencyCode CurrencyCode `json:"currency_code"` 116 | 117 | TrackingURL otils.NullableString `json:"tracking_url"` 118 | 119 | Items []*Item `json:"items"` 120 | 121 | Pickup *Endpoint `json:"pickup"` 122 | Dropoff *Endpoint `json:"dropoff"` 123 | 124 | CreatedAt uint64 `json:"created_at"` 125 | 126 | // Batch is an optional object which 127 | // indicates whether a delivery should be 128 | // batched with other deliveries at pickup. 129 | Batch *Batch `json:"batch"` 130 | } 131 | 132 | type Batch struct { 133 | // Unique identifier of the batch. Deliveries 134 | // in the same batch share the same identifier. 135 | ID string `json:"batch_id"` 136 | 137 | // Count is the total number of deliveries in this batch. 138 | Count int64 `json:"count"` 139 | 140 | Deliveries []string `json:"deliveries,omitempty"` 141 | } 142 | 143 | var ( 144 | errNilPickup = errors.New("a non-nil pickup is required") 145 | errNilDropoff = errors.New("a non-nil pickup is required") 146 | 147 | errNilEndpointLocation = errors.New("a non-nil endpoint.location is required") 148 | errNilEndpointContact = errors.New("a non-nil endpoint.contact is required") 149 | 150 | errInvalidItems = errors.New("expecting at least one valid item") 151 | ) 152 | 153 | func (dr *DeliveryRequest) Validate() error { 154 | if dr == nil || dr.Pickup == nil { 155 | return errNilPickup 156 | } 157 | if err := dr.Pickup.Validate(); err != nil { 158 | return err 159 | } 160 | if err := dr.Dropoff.Validate(); err != nil { 161 | return err 162 | } 163 | if !atLeastOneValidItem(dr.Items) { 164 | return errInvalidItems 165 | } 166 | return nil 167 | } 168 | 169 | var ( 170 | errInvalidQuantity = errors.New("quantity has to be > 0") 171 | errBlankItemTitle = errors.New("item title has to be non-empty") 172 | ) 173 | 174 | func (i *Item) Validate() error { 175 | if i == nil || i.Quantity <= 0 { 176 | return errInvalidQuantity 177 | } 178 | if i.Title == "" { 179 | return errBlankItemTitle 180 | } 181 | return nil 182 | } 183 | 184 | func atLeastOneValidItem(items []*Item) bool { 185 | for _, item := range items { 186 | if err := item.Validate(); err == nil { 187 | return true 188 | } 189 | } 190 | return false 191 | } 192 | 193 | func (e *Endpoint) Validate() error { 194 | if e == nil || e.Location == nil { 195 | return errNilEndpointLocation 196 | } 197 | if e.Contact == nil { 198 | return errNilEndpointContact 199 | } 200 | return nil 201 | } 202 | 203 | func (c *Client) RequestDelivery(req *DeliveryRequest) (*Delivery, error) { 204 | if err := req.Validate(); err != nil { 205 | return nil, err 206 | } 207 | 208 | blob, err := json.Marshal(req) 209 | if err != nil { 210 | return nil, err 211 | } 212 | theURL := fmt.Sprintf("%s/deliveries", c.baseURL()) 213 | httpReq, err := http.NewRequest("POST", theURL, bytes.NewReader(blob)) 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | blob, _, err = c.doHTTPReq(httpReq) 219 | if err != nil { 220 | return nil, err 221 | } 222 | dRes := new(Delivery) 223 | if err := json.Unmarshal(blob, dRes); err != nil { 224 | return nil, err 225 | } 226 | return dRes, nil 227 | } 228 | 229 | var errBlankDeliveryID = errors.New("expecting a non-blank deliveryID") 230 | 231 | // CancelDelivery cancels a delivery referenced by its ID. There are 232 | // potential cancellation fees associated. 233 | // See https://developer.uber.com/docs/deliveries/faq for more information. 234 | func (c *Client) CancelDelivery(deliveryID string) error { 235 | deliveryID = strings.TrimSpace(deliveryID) 236 | if deliveryID == "" { 237 | return errBlankDeliveryID 238 | } 239 | theURL := fmt.Sprintf("%s/deliveries/%s/cancel", c.baseURL(), deliveryID) 240 | httpReq, err := http.NewRequest("POST", theURL, nil) 241 | if err != nil { 242 | return err 243 | } 244 | _, _, err = c.doHTTPReq(httpReq) 245 | return err 246 | } 247 | 248 | type DeliveryListRequest struct { 249 | Status Status `json:"status,omitempty"` 250 | LimitPerPage int64 `json:"limit"` 251 | MaxPageNumber int64 `json:"max_page,omitempty"` 252 | StartOffset int64 `json:"offset"` 253 | 254 | ThrottleDurationMs int64 `json:"throttle_duration_ms"` 255 | } 256 | 257 | type DeliveryThread struct { 258 | Pages chan *DeliveryPage `json:"-"` 259 | Cancel func() 260 | } 261 | 262 | type DeliveryPage struct { 263 | Err error `json:"error"` 264 | PageNumber int64 `json:"page_number,omitempty"` 265 | Deliveries []*Delivery `json:"deliveries,omitempty"` 266 | } 267 | 268 | type recvDelivery struct { 269 | Count int64 `json:"count"` 270 | NextPageQuery string `json:"next_page"` 271 | PreviousPageQuery string `json:"previous_page"` 272 | Deliveries []*Delivery `json:"deliveries"` 273 | } 274 | 275 | type deliveryPager struct { 276 | Offset int64 `json:"offset"` 277 | Limit int64 `json:"limit"` 278 | Status Status `json:"status"` 279 | } 280 | 281 | const ( 282 | NoThrottle = -1 283 | 284 | defaultThrottleDurationMs = 150 * time.Millisecond 285 | ) 286 | 287 | // ListDeliveries requires authorization with OAuth2.0 with 288 | // the delivery scope set. 289 | func (c *Client) ListDeliveries(dReq *DeliveryListRequest) (*DeliveryThread, error) { 290 | if dReq == nil { 291 | dReq = &DeliveryListRequest{Status: StatusReceiptReady} 292 | } 293 | 294 | baseURL := c.legacyV1BaseURL() 295 | fullURL := fmt.Sprintf("%s/deliveries", baseURL) 296 | qv, err := otils.ToURLValues(&deliveryPager{ 297 | Limit: dReq.LimitPerPage, 298 | Status: dReq.Status, 299 | Offset: dReq.StartOffset, 300 | }) 301 | if err != nil { 302 | return nil, err 303 | } 304 | 305 | if len(qv) > 0 { 306 | fullURL = fmt.Sprintf("%s/deliveries?%s", baseURL, qv.Encode()) 307 | } 308 | 309 | parsedURL, err := url.Parse(fullURL) 310 | if err != nil { 311 | return nil, err 312 | } 313 | parsedBaseURL, err := url.Parse(baseURL) 314 | if err != nil { 315 | return nil, err 316 | } 317 | 318 | var errsList []string 319 | if want, got := parsedBaseURL.Scheme, parsedURL.Scheme; got != want { 320 | errsList = append(errsList, fmt.Sprintf("gotScheme=%q wantBaseScheme=%q", got, want)) 321 | } 322 | if want, got := parsedBaseURL.Host, parsedURL.Host; got != want { 323 | errsList = append(errsList, fmt.Sprintf("gotHost=%q wantBaseHost=%q", got, want)) 324 | } 325 | if len(errsList) > 0 { 326 | return nil, errors.New(strings.Join(errsList, "\n")) 327 | } 328 | 329 | maxPage := dReq.MaxPageNumber 330 | pageExceeded := func(pageNumber int64) bool { 331 | return maxPage > 0 && pageNumber >= maxPage 332 | } 333 | 334 | fullDeliveriesBaseURL := fmt.Sprintf("%s/deliveries", baseURL) 335 | resChan := make(chan *DeliveryPage) 336 | cancelChan, cancelFn := makeCancelParadigm() 337 | 338 | go func() { 339 | defer close(resChan) 340 | 341 | pageNumber := int64(0) 342 | throttleDurationMs := defaultThrottleDurationMs 343 | if dReq.ThrottleDurationMs == NoThrottle { 344 | throttleDurationMs = 0 345 | } else { 346 | throttleDurationMs = time.Duration(dReq.ThrottleDurationMs) * time.Millisecond 347 | } 348 | 349 | for { 350 | page := &DeliveryPage{PageNumber: pageNumber} 351 | 352 | req, err := http.NewRequest("GET", fullURL, nil) 353 | if err != nil { 354 | page.Err = err 355 | resChan <- page 356 | return 357 | } 358 | 359 | slurp, _, err := c.doReq(req) 360 | if err != nil { 361 | page.Err = err 362 | resChan <- page 363 | return 364 | } 365 | 366 | recv := new(recvDelivery) 367 | if err := json.Unmarshal(slurp, recv); err != nil { 368 | page.Err = err 369 | resChan <- page 370 | return 371 | } 372 | 373 | page.Deliveries = recv.Deliveries 374 | resChan <- page 375 | pageNumber += 1 376 | pageToken := recv.NextPageQuery 377 | if pageExceeded(pageNumber) || pageToken == "" || len(recv.Deliveries) == 0 { 378 | return 379 | } 380 | 381 | fullURL = fmt.Sprintf("%s?%s", fullDeliveriesBaseURL, pageToken) 382 | 383 | select { 384 | case <-cancelChan: 385 | return 386 | case <-time.After(throttleDurationMs): 387 | } 388 | } 389 | }() 390 | 391 | return &DeliveryThread{Cancel: cancelFn, Pages: resChan}, nil 392 | } 393 | -------------------------------------------------------------------------------- /v1/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | "time" 22 | 23 | "github.com/orijtech/otils" 24 | ) 25 | 26 | type ActivationStatus string 27 | 28 | const ( 29 | Other ActivationStatus = "other" 30 | Onboarding ActivationStatus = "onboarding" 31 | Active ActivationStatus = "active" 32 | InActive ActivationStatus = "inactive" 33 | ) 34 | 35 | const driverV1API = "v1" 36 | 37 | func (c *Client) DriverProfile() (*Profile, error) { 38 | return c.retrieveProfile("/partners/me", driverV1API) 39 | } 40 | 41 | type PaymentCategory string 42 | 43 | const ( 44 | CategoryFare PaymentCategory = "fare" 45 | CategoryDevicePayment PaymentCategory = "device_payment" 46 | CategoryVehiclePayment PaymentCategory = "vehicle_payment" 47 | CategoryPromotion PaymentCategory = "promotion" 48 | CategoryOther PaymentCategory = "other" 49 | ) 50 | 51 | const ( 52 | defaultDriverPaymentsLimitPerPage = 50 53 | 54 | defaultThrottleDuration = 150 * time.Millisecond 55 | ) 56 | 57 | type DriverInfoResponse struct { 58 | Cancel func() 59 | Pages <-chan *DriverInfoPage 60 | } 61 | 62 | type driverInfoWrap struct { 63 | Count int `json:"count"` 64 | Limit int `json:"limit"` 65 | Offset int `json:"offset"` 66 | Payments []*Payment `json:"payments"` 67 | Trips []*Trip `json:"trips"` 68 | } 69 | 70 | func (dpq *DriverInfoQuery) toRealDriverQuery() *realDriverQuery { 71 | rdpq := &realDriverQuery{ 72 | Offset: dpq.Offset, 73 | } 74 | if dpq.StartDate != nil { 75 | rdpq.StartTimeUnix = dpq.StartDate.Unix() 76 | } 77 | if dpq.EndDate != nil { 78 | rdpq.EndTimeUnix = dpq.EndDate.Unix() 79 | } 80 | return rdpq 81 | } 82 | 83 | // realDriverQuery because it is the 1-to-1 match 84 | // with the fields sent it to query for the payments. 85 | // DriverQuery is just there for convenience and 86 | // easy API usage from callers e.g passing in a date without 87 | // having to worry about its exact Unix timestamp. 88 | type realDriverQuery struct { 89 | Offset int `json:"offset,omitempty"` 90 | LimitPerPage int `json:"limit,omitempty"` 91 | StartTimeUnix int64 `json:"from_time,omitempty"` 92 | EndTimeUnix int64 `json:"to_time,omitempty"` 93 | } 94 | 95 | type DriverInfoQuery struct { 96 | Offset int `json:"offset,omitempty"` 97 | 98 | // LimitPerPage is the number of items to retrieve per page. 99 | // Default is 5, maximum is 50. 100 | LimitPerPage int `json:"limit,omitempty"` 101 | 102 | StartDate *time.Time `json:"start_date,omitempty"` 103 | EndDate *time.Time `json:"end_date,omitempty"` 104 | 105 | MaxPageNumber int `json:"max_page_number,omitempty"` 106 | 107 | Throttle time.Duration `json:"throttle,omitempty"` 108 | } 109 | 110 | type DriverInfoPage struct { 111 | PageNumber int `json:"page_number,omitempty"` 112 | Payments []*Payment `json:"payments,omitempty"` 113 | Trips []*Trip `json:"trips,omitempty"` 114 | Err error `json:"error"` 115 | } 116 | 117 | func (c *Client) ListDriverTrips(dpq *DriverInfoQuery) (*DriverInfoResponse, error) { 118 | return c.listDriverInfo(dpq, "/partners/trips") 119 | } 120 | 121 | // DriverPayments returns the payments for the given driver. 122 | // Payments are available at this endpoint in near real-time. Some entries, 123 | // such as "device_subscription" will appear on a periodic basis when actually 124 | // billed to the parnter. If a trip is cancelled (either by rider or driver) and 125 | // there is no payment made, the corresponding "trip_id" of that cancelled trip 126 | // will not appear in this endpoint. If the given driver works for a fleet manager, 127 | // there will be no payments associated and the response will always be an empty 128 | // array. Drivers working for fleet managers will receive payments from the fleet 129 | // manager and not from Uber. 130 | func (c *Client) ListDriverPayments(dpq *DriverInfoQuery) (*DriverInfoResponse, error) { 131 | return c.listDriverInfo(dpq, "/partners/payments") 132 | } 133 | 134 | func (c *Client) listDriverInfo(dpq *DriverInfoQuery, path string) (*DriverInfoResponse, error) { 135 | if dpq == nil { 136 | dpq = new(DriverInfoQuery) 137 | } 138 | 139 | throttleDuration := dpq.Throttle 140 | if throttleDuration == NoThrottle { 141 | throttleDuration = 0 142 | } else if throttleDuration <= 0 { 143 | throttleDuration = defaultThrottleDuration 144 | } 145 | 146 | maxPageNumber := dpq.MaxPageNumber 147 | pageExceeds := func(pageNumber int) bool { 148 | return maxPageNumber > 0 && pageNumber >= maxPageNumber 149 | } 150 | 151 | baseURL := fmt.Sprintf("%s%s", c.baseURL(driverV1API), path) 152 | rdpq := dpq.toRealDriverQuery() 153 | limitPerPage := rdpq.LimitPerPage 154 | if limitPerPage <= 0 { 155 | limitPerPage = defaultDriverPaymentsLimitPerPage 156 | } 157 | 158 | cancelChan, cancelFn := makeCancelParadigm() 159 | resChan := make(chan *DriverInfoPage) 160 | go func() { 161 | defer close(resChan) 162 | 163 | pageNumber := 0 164 | 165 | for { 166 | curPage := new(DriverInfoPage) 167 | curPage.PageNumber = pageNumber 168 | 169 | qv, err := otils.ToURLValues(rdpq) 170 | if err != nil { 171 | curPage.Err = err 172 | resChan <- curPage 173 | return 174 | } 175 | 176 | fullURL := baseURL 177 | if len(qv) > 0 { 178 | fullURL += "?" + qv.Encode() 179 | } 180 | 181 | req, err := http.NewRequest("GET", fullURL, nil) 182 | if err != nil { 183 | curPage.Err = err 184 | resChan <- curPage 185 | return 186 | } 187 | blob, _, err := c.doAuthAndHTTPReq(req) 188 | if err != nil { 189 | curPage.Err = err 190 | resChan <- curPage 191 | return 192 | } 193 | 194 | recv := new(driverInfoWrap) 195 | if err := json.Unmarshal(blob, recv); err != nil { 196 | curPage.Err = err 197 | resChan <- curPage 198 | return 199 | } 200 | 201 | // No payments nor trips sent back, so a sign that we are at the end 202 | if len(recv.Payments) == 0 && len(recv.Trips) == 0 { 203 | return 204 | } 205 | 206 | curPage.Trips = recv.Trips 207 | curPage.Payments = recv.Payments 208 | 209 | resChan <- curPage 210 | 211 | pageNumber += 1 212 | if pageExceeds(pageNumber) { 213 | return 214 | } 215 | 216 | select { 217 | case <-cancelChan: 218 | return 219 | case <-time.After(throttleDuration): 220 | } 221 | 222 | rdpq.Offset += recv.Limit 223 | } 224 | }() 225 | 226 | resp := &DriverInfoResponse{ 227 | Cancel: cancelFn, 228 | Pages: resChan, 229 | } 230 | 231 | return resp, nil 232 | } 233 | -------------------------------------------------------------------------------- /v1/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "errors" 19 | "log" 20 | ) 21 | 22 | type ActionableError struct { 23 | msg string 24 | code int 25 | action string 26 | retryable bool 27 | signature string 28 | } 29 | 30 | var _ error = (*ActionableError)(nil) 31 | 32 | func (ae *ActionableError) Error() string { 33 | if ae == nil { 34 | return "" 35 | } 36 | 37 | return ae.msg 38 | } 39 | 40 | func (ae *ActionableError) HasAction() bool { 41 | return ae != nil && len(ae.action) > 0 42 | } 43 | 44 | func (ae *ActionableError) Action() string { 45 | if ae == nil { 46 | return "" 47 | } 48 | return ae.action 49 | } 50 | 51 | var ( 52 | ErrProccessingRequest = errors.New("error_") 53 | ) 54 | 55 | // UberErrors 56 | // * 400 :: Unconfirmed email :: The user hasn't confirmed their email address. 57 | // Instruct them to confirm their email by visiting 58 | // https://riders.uber.com or within the native mobile 59 | // application. 60 | var ErrUnconfirmedEmail = &ActionableError{ 61 | msg: "unconfirmed email address", 62 | code: 400, 63 | signature: "unconfirmed_email", 64 | action: "https://riders.uber.com", 65 | } 66 | 67 | // * 400 :: error_processing_request :: Error processing the request. 68 | var ErrProcessingRequest = &ActionableError{ 69 | msg: "encountered a error processing the request", 70 | code: 400, 71 | signature: "error_processing_request", 72 | } 73 | 74 | // * 400 :: promotions_revoked :: Promotions revoked. 75 | var ErrPromotionsRevoked = &ActionableError{ 76 | msg: "encountered a error processing the request", 77 | code: 400, 78 | signature: "promotions_revoked", 79 | } 80 | 81 | // * 400 :: invalid_payment :: The rider's payment method is invalid. The user 82 | // must update the billing info. This could include e.g 83 | // Android Pay. 84 | var ErrInvalidPayment = &ActionableError{ 85 | msg: "rider's payment is invalid. Please update billing information", 86 | code: 400, 87 | signature: "invalid_payment", 88 | } 89 | 90 | // * 400 :: invalid_payment_method :: The provided payment method is not valid. 91 | var ErrInvalidPaymentMethod = &ActionableError{ 92 | msg: "the provided payment method is not valid", 93 | code: 400, 94 | signature: "invalid_payment_method", 95 | } 96 | 97 | // * 400 :: outstanding_balance_update_billing :: The user has outstanding balances. 98 | // The user must update the billing info. 99 | var ErrOutstandingBalance = &ActionableError{ 100 | msg: "your account has outstanding balances. Please update billing information", 101 | code: 400, 102 | signature: "outstanding_balance_update_billing", 103 | } 104 | 105 | // * 400 :: insufficient_balance :: There is insufficient balance on the credit card associated 106 | // with the user. The user must update the billing info. 107 | var ErrInsufficientBalance = &ActionableError{ 108 | msg: "insufficient balance on the credit card associated with your account. Please update billing information", 109 | code: 400, 110 | signature: "insufficient_balance", 111 | } 112 | 113 | // * 400 :: payment_method_not_allowed :: The payment method is not allowed. 114 | var ErrPaymentMethodNotAllowed = &ActionableError{ 115 | msg: "the payment method is not allowed", 116 | code: 400, 117 | signature: "payment_method_not_allowed", 118 | } 119 | 120 | // * 400 :: card_assoc_outstanding_balance :: The user's associated card has an outstanding balance. 121 | // The user must update the billing info. 122 | var ErrCardHasOutstandingBalance = &ActionableError{ 123 | msg: "the associated card has an outstanding balance. Please update billing information", 124 | code: 400, 125 | signature: "card_assoc_outstanding_balance", 126 | } 127 | 128 | // * 400 :: invalid_mobile_phone_number :: The user's mobile phone number is not supported. We don't 129 | // allow phone numbers for some providers that allow the creation 130 | // of temporary phone numbers. 131 | var ErrInvalidMobilePhoneNumber = &ActionableError{ 132 | msg: "the mobile phone number is not supported. We don't allow phone numbers for some providers that allow the creation of temporary phone numbers", 133 | code: 400, 134 | signature: "invalid_mobile_phone_number", 135 | } 136 | 137 | // * 403 :: forbidden :: This user is forbidden from making a request at this time and should consult 138 | // our support team by visiting https://help.uber.com or by emailing support@uber.com 139 | var ErrForbiddenRequest = &ActionableError{ 140 | msg: "you are forbidden from making a request at this time. Please consult our support team", 141 | code: 403, 142 | signature: "forbidden", 143 | action: "https://help.uber.com,support@uber.com", 144 | } 145 | 146 | // * 403 :: unverified :: The user hasn't confirmed their phone number. Instruct the user to 147 | // confirm their mobile phone number within the native mobile app 148 | // or by visiting https://riders.uber.com 149 | var ErrUnverified = &ActionableError{ 150 | msg: "your phone number hasn't yet been confirmed", 151 | code: 403, 152 | signature: "unverified", 153 | action: "https://riders.uber.com", 154 | } 155 | 156 | // * 403 :: verification_required :: The user currently cannot make ride requests through 157 | // the API and is advised to use the Uber iOS or Android 158 | // rider app to get a ride. 159 | var ErrVerificationRequired = &ActionableError{ 160 | msg: "you aren't allowed to make ride requests through the API. Please use the Uber iOS or Android rider app to get a ride", 161 | code: 403, 162 | signature: "verification_required", 163 | } 164 | 165 | // * 403 :: product_not_allowed :: The product being requested is not available to the user. 166 | // Have them select a different product to successfully 167 | // make a request. 168 | var ErrProductNotAllowed = &ActionableError{ 169 | msg: "the requested product is not available to you unfortunately. Please select another product", 170 | code: 403, 171 | signature: "product_not_allowed", 172 | } 173 | 174 | // * 403 :: pay_balance :: The rider has an outstanding balance and must update their 175 | // account settings by using the native mobile application or 176 | // by visiting https://riders.uber.com 177 | var ErrPayBalance = &ActionableError{ 178 | msg: "you have an outstanding balance. Please update your account settings", 179 | code: 403, 180 | signature: "pay_balance", 181 | action: "https://riders.uber.com", 182 | } 183 | 184 | // * 403 :: user_not_allowed :: The user is banned and not permitted to request a ride. 185 | var ErrUserNotAllowed = &ActionableError{ 186 | msg: "unfortunately you are banned and not permitted to request a ride", 187 | code: 403, 188 | signature: "user_not_allowed", 189 | } 190 | 191 | // * 403 :: too_many_cancellations :: The rider is temporarily blocked due to canceling 192 | // too many times. 193 | var ErrTooManyCancellations = &ActionableError{ 194 | msg: "you are temporarily blocked for canceling too many times", 195 | code: 403, 196 | signature: "too_many_cancellations", 197 | } 198 | 199 | // * 403 :: missing_national_id :: Certain jurisdictions require Uber users to register 200 | // their national ID number or passport number before 201 | // taking a ride. If a user receives this error when 202 | // booking a trip through the Developer API, they must 203 | // enter their national ID number or passport number 204 | // through the Uber iOS or Android app. 205 | var ErrMissingNationalID = &ActionableError{ 206 | msg: "certain jurisdictions require registration of your national ID or passport number before taking a ride. Please enter your national ID or passport number through the Uber iOS or Android app", 207 | code: 403, 208 | signature: "missing_national_id", 209 | } 210 | 211 | // * 404 :: no_product_found :: An invalid product ID was requested. Retry the API call 212 | // with a valid product ID. 213 | var ErrNoProductFound = &ActionableError{ 214 | msg: "an invalid product ID was requested. Retry the API call with a valid product ID", 215 | code: 404, 216 | signature: "no_product_found", 217 | } 218 | 219 | // * 409 :: missing_payment_method :: The rider must have at least one payment method 220 | // on file to request a car. The rider must add a 221 | // payment method by using the native mobile application 222 | // or by visiting https://riders.uber.com 223 | var ErrMissingPaymentMethod = &ActionableError{ 224 | msg: "please add at least one payment method on file before requesting a car", 225 | code: 409, 226 | signature: "missing_payment_method", 227 | action: "https://riders.uber.com", 228 | } 229 | 230 | // * 409 :: surge :: Surge pricing is currently in effect for this product. Please have 231 | // the user confirm surge pricing by sending them to the surge_confirmation 232 | // href described. 233 | var ErrSurge = &ActionableError{ 234 | msg: "surge pricing is currently in effect for this product. Please confirm the surge pricing first", 235 | code: 409, 236 | signature: "surge", 237 | } 238 | 239 | // * 409 :: fare_expired :: The fare has expired for the requested product. Please get estimates 240 | // again, confirm the new fare, and then re-request. 241 | var ErrFareExpired = &ActionableError{ 242 | msg: "the fare has expired for the requested product. Please get estimates again, confirm the new fare and then re-request", 243 | code: 409, 244 | signature: "fare_expired", 245 | } 246 | 247 | // * 409 :: retry_request :: An error has occured when attempting to request a product. 248 | // Please reattempt the request on behalf of the user. 249 | var ErrRetryRequest = &ActionableError{ 250 | msg: "an error has occured when attempting to reques a product. Please retry the request", 251 | code: 409, 252 | signature: "retry_request", 253 | retryable: true, 254 | } 255 | 256 | // * 409 :: current_trip_exists :: The user is currently on a trip. 257 | var ErrUserCurrentlyOnTrip = &ActionableError{ 258 | msg: "the user is currently on a trip", 259 | code: 409, 260 | signature: "current_trip_exists", 261 | } 262 | 263 | // * 422 :: invalid_fare_id :: This fare id is invalid or expired. 264 | var ErrInvalidFareID = &ActionableError{ 265 | msg: "the fare id is invalid or expired", 266 | code: 422, 267 | signature: "invalid_fare_id", 268 | } 269 | 270 | // * 422 :: destination_required :: This product requires setting a destination 271 | // for ride requests. 272 | var ErrDestinationRequired = &ActionableError{ 273 | msg: "this product requires setting a destination", 274 | code: 422, 275 | signature: "destination_required", 276 | } 277 | 278 | // * 422 :: distance_exceeded :: The distance between start and end locations exceeds 100 miles. 279 | var ErrDistanceExceeded = &ActionableError{ 280 | msg: "the distance between start and end location exceeds 100 miles", 281 | code: 422, 282 | signature: "distance_exceeded", 283 | } 284 | 285 | // * 422 :: same_pickup_dropoff :: Pickup and Dropoff can't be the same. 286 | var ErrSamePickupAsDroOff = &ActionableError{ 287 | msg: "pickup and dropoff cannot be the same", 288 | code: 422, 289 | signature: "same_pickup_dropoff", 290 | } 291 | 292 | // * 422 :: validation_failed :: The destination is not supported for uberPOOL. 293 | var ErrInvalidUberPoolDestination = &ActionableError{ 294 | msg: "this destination is not supported for uberPOOL", 295 | code: 422, 296 | signature: "validation_failed", 297 | } 298 | 299 | // * 422 :: invalid_seat_count :: Number of seats exceeds max capacity. 300 | var ErrInvalidSeatCount = &ActionableError{ 301 | msg: "number of seats exceeds max capacity", 302 | code: 422, 303 | signature: "invalid_seat_count", 304 | } 305 | 306 | // * 422 :: outside_service_area :: The destination is not supported by the requested product. 307 | var ErrDestinationOutsideServiceArea = &ActionableError{ 308 | msg: "the destination is not supported by the requested product", 309 | code: 422, 310 | signature: "outside_service_area", 311 | } 312 | 313 | // * 500 :: internal_server_error :: An unknown error has occured. 314 | var ErrInternalServerError = &ActionableError{ 315 | msg: "an unknown error has occured", 316 | code: 500, 317 | signature: "internal_server_error", 318 | } 319 | 320 | var actionableErrorsIndex map[string]*ActionableError 321 | 322 | var actionableErrsList = [...]*ActionableError{ 323 | 0: ErrUnconfirmedEmail, 324 | 1: ErrProcessingRequest, 325 | 2: ErrPromotionsRevoked, 326 | 3: ErrInvalidPayment, 327 | 4: ErrInvalidPaymentMethod, 328 | 5: ErrOutstandingBalance, 329 | 6: ErrInsufficientBalance, 330 | 7: ErrPaymentMethodNotAllowed, 331 | 8: ErrCardHasOutstandingBalance, 332 | 9: ErrInvalidMobilePhoneNumber, 333 | 10: ErrForbiddenRequest, 334 | 11: ErrUnverified, 335 | 12: ErrVerificationRequired, 336 | 13: ErrProductNotAllowed, 337 | 14: ErrPayBalance, 338 | 15: ErrUserNotAllowed, 339 | 16: ErrTooManyCancellations, 340 | 17: ErrMissingNationalID, 341 | 18: ErrNoProductFound, 342 | 19: ErrMissingPaymentMethod, 343 | 20: ErrSurge, 344 | 21: ErrFareExpired, 345 | 22: ErrRetryRequest, 346 | 23: ErrUserCurrentlyOnTrip, 347 | 24: ErrInvalidFareID, 348 | 25: ErrDestinationRequired, 349 | 26: ErrDistanceExceeded, 350 | 27: ErrSamePickupAsDroOff, 351 | 28: ErrInvalidUberPoolDestination, 352 | 29: ErrInvalidSeatCount, 353 | 30: ErrDestinationOutsideServiceArea, 354 | 31: ErrInternalServerError, 355 | } 356 | 357 | func init() { 358 | actionableErrorsIndex = make(map[string]*ActionableError) 359 | for i, ae := range actionableErrsList { 360 | _, previouslyIn := actionableErrorsIndex[ae.signature] 361 | if previouslyIn { 362 | log.Fatalf("actionableError: %#d signature (%q) already exists", i, ae.signature) 363 | } 364 | actionableErrorsIndex[ae.signature] = ae 365 | } 366 | } 367 | 368 | func lookupErrorBySignature(signature string) *ActionableError { 369 | return actionableErrorsIndex[signature] 370 | } 371 | -------------------------------------------------------------------------------- /v1/errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestErrorLookups(t *testing.T) { 22 | for i, ae := range actionableErrsList { 23 | signature := ae.signature 24 | retrAE := lookupErrorBySignature(signature) 25 | if retrAE != ae { 26 | t.Errorf("#%d: %q signature lookup returned (%#v) wanted (%#v)", i, signature, retrAE, ae) 27 | } 28 | } 29 | } 30 | 31 | func TestRandomSignatures(t *testing.T) { 32 | tests := [...]struct { 33 | signature string 34 | want *ActionableError 35 | }{ 36 | 0: {"unconfirmed_email", ErrUnconfirmedEmail}, 37 | 1: {"xyz", nil}, 38 | 2: {"too_many_cancellations", ErrTooManyCancellations}, 39 | 3: {"missing_national_id", ErrMissingNationalID}, 40 | } 41 | 42 | for i, tt := range tests { 43 | got := lookupErrorBySignature(tt.signature) 44 | want := tt.want 45 | if got != want { 46 | t.Errorf("#%d: got=%v want=%v", i, got, want) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /v1/history.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | "time" 22 | 23 | "github.com/orijtech/otils" 24 | ) 25 | 26 | type Trip struct { 27 | // Status of the activity. As per API v1.2, 28 | // it only return "completed" for now. 29 | Status Status `json:"status,omitempty"` 30 | 31 | // Length of activity in miles. 32 | DistanceMiles float64 `json:"distance,omitempty"` 33 | 34 | // UnixTimestamp of activity start time. 35 | StartTimeUnix int64 `json:"start_time,omitempty"` 36 | 37 | // UnixTimestamp of activity end time. 38 | EndTimeUnix int64 `json:"end_time,omitempty"` 39 | 40 | // The city in which this trip was initiated. 41 | StartCity *Place `json:"start_city,omitempty"` 42 | 43 | ProductID string `json:"product_id,omitempty"` 44 | RequestID string `json:"request_id,omitempty"` 45 | TripID string `json:"trip_id,omitempty"` 46 | DriverID string `json:"driver_id,omitempty"` 47 | 48 | Unit string `json:"distance_unit,omitempty"` 49 | 50 | Duration otils.NullableFloat64 `json:"duration,omitempty"` 51 | DurationEstimate otils.NullableFloat64 `json:"duration_estimate,omitempty"` 52 | 53 | Distance otils.NullableFloat64 `json:"distance,omitempty"` 54 | DistanceEstimate otils.NullableFloat64 `json:"distance_estimate,omitempty"` 55 | 56 | VehicleID otils.NullableString `json:"vehicle_id,omitempty"` 57 | 58 | SurgeMultiplier otils.NullableFloat64 `json:"surge_multiplier,omitempty"` 59 | Fare otils.NullableFloat64 `json:"fare,omitempty"` 60 | Dropoff *Endpoint `json:"dropoff,omitempty"` 61 | Pickup *Endpoint `json:"pickup,omitempty"` 62 | StatusChanges []*StatusChange `json:"status_changes,omitempty"` 63 | CurrencyCode CurrencyCode `json:"currency_code,omitempty"` 64 | 65 | // The values below are exclusively populated 66 | // when requested for the current trip or by tripID. 67 | Shared bool `json:"shared,omitempty"` 68 | 69 | // Vehicle will only be non-nil during an ongoing trip. 70 | Vehicle *Vehicle `json:"vehicle,omitempty"` 71 | 72 | // Driver will only be non-nil during an ongoing trip. 73 | Driver *Driver `json:"driver,omitempty"` 74 | 75 | // Location contains the location information 76 | // about the driver in the vehicle. 77 | Location *Location `json:"location,omitempty"` 78 | Destination *Location `json:"destination,omitempty"` 79 | 80 | Riders []*Profile `json:"riders,omitempty"` 81 | 82 | // Waypoints is the list of waypoints containing: 83 | // + lat/lng of different waypoints 84 | // + the rider involved 85 | // + type of waypoint 86 | // It is only returned for shared rides like UberPOOL. 87 | Waypoints []*Location `json:"waypoints,omitempty"` 88 | } 89 | 90 | type StatusChange struct { 91 | Status Status `json:"status,omitempty"` 92 | TimestampUnix int64 `json:"timestamp,omitempty"` 93 | } 94 | 95 | type Place struct { 96 | // The latitude of the approximate city center. 97 | Latitude float64 `json:"latitude,omitempty"` 98 | 99 | Name string `json:"display_name,omitempty"` 100 | 101 | // The longitude of the approximate city center. 102 | Longitude float64 `json:"longitude,omitempty"` 103 | 104 | Address string `json:"address,omitempty"` 105 | } 106 | 107 | type TripThread struct { 108 | Trips []*Trip `json:"history"` 109 | Count int64 `json:"count"` 110 | Limit int64 `json:"limit"` 111 | Offset int64 `json:"offset"` 112 | } 113 | 114 | type Pager struct { 115 | ThrottleDuration time.Duration `json:"-"` 116 | LimitPerPage int64 `json:"limit"` 117 | MaxPages int64 `json:"-"` 118 | StartOffset int64 `json:"offset"` 119 | } 120 | 121 | type TripThreadPage struct { 122 | TripThread 123 | Err error 124 | PageNumber uint64 125 | } 126 | 127 | const ( 128 | DefaultLimitPerPage = int64(50) 129 | DefaultStartOffset = int64(0) 130 | ) 131 | 132 | func (treq *Pager) adjustPageParams() { 133 | if treq.LimitPerPage <= 0 { 134 | treq.LimitPerPage = DefaultLimitPerPage 135 | } 136 | if treq.StartOffset <= 0 { 137 | treq.StartOffset = DefaultStartOffset 138 | } 139 | } 140 | 141 | func (c *Client) ListAllMyHistory() (thChan chan *TripThreadPage, cancelFn func(), err error) { 142 | return c.ListHistory(nil) 143 | } 144 | 145 | func (c *Client) ListHistory(threq *Pager) (thChan chan *TripThreadPage, cancelFn func(), err error) { 146 | treq := new(Pager) 147 | if threq != nil { 148 | *treq = *threq 149 | } 150 | 151 | // Adjust the paging parameters since they'll be heavily used 152 | treq.adjustPageParams() 153 | 154 | requestedMaxPage := treq.MaxPages 155 | pageNumberExceeds := func(pageNumber uint64) bool { 156 | if requestedMaxPage <= 0 { 157 | // No page limit requested at all here 158 | return false 159 | } 160 | 161 | return pageNumber >= uint64(requestedMaxPage) 162 | } 163 | 164 | cancelChan, cancelFn := makeCancelParadigm() 165 | 166 | historyChan := make(chan *TripThreadPage) 167 | go func() { 168 | defer close(historyChan) 169 | 170 | throttleDuration := 150 * time.Millisecond 171 | pageNumber := uint64(0) 172 | 173 | canPage := true 174 | for canPage { 175 | ttp := new(TripThreadPage) 176 | ttp.PageNumber = pageNumber 177 | 178 | qv, err := otils.ToURLValues(treq) 179 | if err != nil { 180 | ttp.Err = err 181 | historyChan <- ttp 182 | return 183 | } 184 | 185 | fullURL := fmt.Sprintf("%s/history?%s", c.baseURL(), qv.Encode()) 186 | req, err := http.NewRequest("GET", fullURL, nil) 187 | if err != nil { 188 | ttp.Err = err 189 | historyChan <- ttp 190 | return 191 | } 192 | 193 | slurp, _, err := c.doReq(req) 194 | if err != nil { 195 | ttp.Err = err 196 | historyChan <- ttp 197 | return 198 | } 199 | 200 | if err := json.Unmarshal(slurp, ttp); err != nil { 201 | ttp.Err = err 202 | historyChan <- ttp 203 | return 204 | } 205 | 206 | historyChan <- ttp 207 | 208 | if ttp.Count <= 0 { 209 | // No more items to page 210 | return 211 | } 212 | 213 | select { 214 | case <-cancelChan: 215 | // The user has canceled the paging 216 | canPage = false 217 | return 218 | 219 | case <-time.After(throttleDuration): 220 | // Do nothing here, the throttle time expired. 221 | } 222 | 223 | // Now it is time to adjust the next offset accordingly with the remaining count 224 | 225 | // Increment the page number as well 226 | pageNumber += 1 227 | if pageNumberExceeds(pageNumber) { 228 | return 229 | } 230 | 231 | treq.StartOffset += treq.LimitPerPage 232 | } 233 | }() 234 | 235 | return historyChan, cancelFn, nil 236 | } 237 | -------------------------------------------------------------------------------- /v1/maps.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "net/http" 22 | 23 | "github.com/skratchdot/open-golang/open" 24 | ) 25 | 26 | type Map struct { 27 | RequestID string `json:"request_id"` 28 | 29 | URL string `json:"href"` 30 | } 31 | 32 | var ( 33 | errEmptyTripID = errors.New("expecting a non-empty tripID") 34 | errNoSuchMap = errors.New("no such map") 35 | ) 36 | 37 | func (c *Client) RequestMap(tripID string) (*Map, error) { 38 | if tripID == "" { 39 | return nil, errEmptyTripID 40 | } 41 | 42 | fullURL := fmt.Sprintf("%s/requests/%s/map", c.baseURL(), tripID) 43 | req, err := http.NewRequest("GET", fullURL, nil) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | slurp, _, err := c.doAuthAndHTTPReq(req) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | uinfo := new(Map) 54 | blankMap := *uinfo 55 | if err := json.Unmarshal(slurp, uinfo); err != nil { 56 | return nil, err 57 | } 58 | if blankMap == *uinfo { 59 | return nil, errNoSuchMap 60 | } 61 | return uinfo, nil 62 | } 63 | 64 | // OpenMapForTrip is a convenience method that opens the map 65 | // for a trip or returns an error if it encounters an error. 66 | func (c *Client) OpenMapForTrip(tripID string) error { 67 | uinfo, err := c.RequestMap(tripID) 68 | if err != nil { 69 | return err 70 | } 71 | return open.Start(uinfo.URL) 72 | } 73 | -------------------------------------------------------------------------------- /v1/payment.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | "strconv" 22 | 23 | "github.com/orijtech/otils" 24 | ) 25 | 26 | type Payment struct { 27 | // ID is the unique identifier of the payment item. 28 | // If the payment is related to a trip, it is the same as TripID. 29 | ID string `json:"payment_id,omitempty"` 30 | MethodID string `json:"payment_method_id,omitempty"` 31 | 32 | Category PaymentCategory `json:"category,omitempty"` 33 | 34 | Description string `json:"description,omitempty"` 35 | PaymentMethod PaymentMethod `json:"type,omitempty"` 36 | 37 | // DriverID is the unique identifier of the 38 | // driver who received or made the payment. 39 | DriverID string `json:"driver_id,omitempty"` 40 | 41 | // PartnerID is the unique identifier of the 42 | // Fleet Manager for the driver. 43 | PartnerID string `json:"partner_id,omitempty"` 44 | 45 | // TripID is the unique identifier of the trip associated 46 | // with the payment. It is only present for 47 | // PaymentCategory `Fare` otherwise it is null. 48 | TripID otils.NullableString `json:"trip_id,omitempty"` 49 | 50 | EventTime otils.NullableFloat64 `json:"event_time,omitempty"` 51 | 52 | // CashCollected is the amount collected in cash by the driver. 53 | // It is only set for Uber products that are enabled for cash payments. 54 | CashCollected otils.NullableFloat64 `json:"cash_collected,omitempty"` 55 | 56 | // Amount is the net payout to the driver. It is positive for 57 | // payments to the account, negative for charges to the account. 58 | Amount otils.NullableFloat64 `json:"amount,omitempty"` 59 | 60 | // CurrencyCode is the ISO 4217 currency code of the payment. 61 | CurrencyCode otils.NullableString `json:"currency_code,omitempty"` 62 | 63 | // Breakdown is the breakdown of the fare. 64 | Breakdown *FareBreakdown `json:"breakdown,omitempty"` 65 | 66 | // RiderFees details the fees paid to the driver by the rider 67 | // These fees are not reflected in the fare. 68 | RiderFees *ServiceFee `json:"rider_fees,omitempty"` 69 | } 70 | 71 | type PaymentMethod uint 72 | 73 | const ( 74 | PaymentUnknown PaymentMethod = iota 75 | 76 | // Last 2 digits of card e.g "***23" or the 77 | // obfuscated email address ("ga***@uber.com") 78 | // depending on the account identifier. 79 | PaymentAlipay 80 | 81 | // Last 2 digits of cards e.g "***23". 82 | PaymentApplePay 83 | PaymentAmericanExpress 84 | PaymentDiscover 85 | PaymentJCB 86 | PaymentLianLian 87 | PaymentMaestro 88 | PaymentMastercard 89 | PaymentPaypal 90 | PaymentPaytm 91 | PaymentUnionPay 92 | PaymentVisa 93 | 94 | // A descriptive name of the family account e.g "John Doe Family Shared". 95 | PaymentUberFamilyAccount 96 | 97 | // No description for these ones. 98 | PaymentAirtel 99 | PaymentAndroidPay 100 | PaymentCash 101 | PaymentUcharge 102 | PaymentZaakpay 103 | ) 104 | 105 | var paymentMethodToString = map[PaymentMethod]string{ 106 | PaymentAirtel: "airtel", 107 | PaymentAlipay: "alipay", 108 | PaymentApplePay: "apple_pay", 109 | PaymentAmericanExpress: "american_express", 110 | PaymentAndroidPay: "android_pay", 111 | PaymentUberFamilyAccount: "family_account", 112 | PaymentCash: "cash", 113 | PaymentDiscover: "discover", 114 | PaymentJCB: "jcb", 115 | PaymentLianLian: "lianlian", 116 | PaymentMaestro: "maestro", 117 | PaymentMastercard: "mastercard", 118 | PaymentPaypal: "paypal", 119 | PaymentPaytm: "paytm", 120 | PaymentUcharge: "ucharge", 121 | PaymentUnionPay: "unionpay", 122 | PaymentUnknown: "unknown", 123 | PaymentVisa: "visa", 124 | PaymentZaakpay: "zaakpay", 125 | } 126 | 127 | var stringToPaymentMethod map[string]PaymentMethod 128 | 129 | func init() { 130 | stringToPaymentMethod = make(map[string]PaymentMethod) 131 | for paymentMethod, str := range paymentMethodToString { 132 | stringToPaymentMethod[str] = paymentMethod 133 | } 134 | } 135 | 136 | func (pm *PaymentMethod) PaymentMethodToString() string { 137 | if pm == nil { 138 | ppm := PaymentUnknown 139 | pm = &ppm 140 | } 141 | return paymentMethodToString[*pm] 142 | } 143 | 144 | func (pm PaymentMethod) String() string { 145 | return pm.PaymentMethodToString() 146 | } 147 | 148 | func StringToPaymentMethod(str string) PaymentMethod { 149 | pm, ok := stringToPaymentMethod[str] 150 | if !ok { 151 | pm = PaymentUnknown 152 | } 153 | return pm 154 | } 155 | 156 | var _ json.Unmarshaler = (*PaymentMethod)(nil) 157 | 158 | func (pm *PaymentMethod) UnmarshalJSON(b []byte) error { 159 | unquoted, err := strconv.Unquote(string(b)) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | *pm = StringToPaymentMethod(unquoted) 165 | return nil 166 | } 167 | 168 | type PaymentListing struct { 169 | Methods []*Payment `json:"payment_methods,omitempty"` 170 | 171 | // The unique identifier of 172 | // the last used payment method. 173 | LastUsedID string `json:"last_used,omitempty"` 174 | } 175 | 176 | func (c *Client) ListPaymentMethods() (*PaymentListing, error) { 177 | fullURL := fmt.Sprintf("%s/payment-methods", c.baseURL()) 178 | req, err := http.NewRequest("GET", fullURL, nil) 179 | if err != nil { 180 | return nil, err 181 | } 182 | req.Header.Set("Content-Type", "application/json") 183 | req.Header.Set("Accept-Language", "en_US") 184 | 185 | slurp, _, err := c.doReq(req) 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | listing := new(PaymentListing) 191 | if err := json.Unmarshal(slurp, listing); err != nil { 192 | return nil, err 193 | } 194 | return listing, nil 195 | } 196 | -------------------------------------------------------------------------------- /v1/places.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "net/http" 23 | ) 24 | 25 | type PlaceName string 26 | 27 | const ( 28 | PlaceHome PlaceName = "home" 29 | PlaceWork PlaceName = "work" 30 | ) 31 | 32 | func (c *Client) Place(placeName PlaceName) (*Place, error) { 33 | fullURL := fmt.Sprintf("%s/places/%s", c.baseURL(), placeName) 34 | req, err := http.NewRequest("GET", fullURL, nil) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return c.doPlaceReq(req) 39 | } 40 | 41 | func (c *Client) doPlaceReq(req *http.Request) (*Place, error) { 42 | slurp, _, err := c.doReq(req) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | place := new(Place) 48 | if err := json.Unmarshal(slurp, place); err != nil { 49 | return nil, err 50 | } 51 | return place, nil 52 | } 53 | 54 | type PlaceParams struct { 55 | Place PlaceName `json:"place"` 56 | Address string `json:"address"` 57 | } 58 | 59 | var ( 60 | errEmptyAddress = errors.New("expecting a non-empty address") 61 | errInvalidPlaceName = fmt.Errorf("invalid placeName; can only be either %q or %q", PlaceHome, PlaceWork) 62 | ) 63 | 64 | func (pp *PlaceParams) Validate() error { 65 | if pp == nil || pp.Address == "" { 66 | return errEmptyAddress 67 | } 68 | 69 | switch pp.Place { 70 | case PlaceHome, PlaceWork: 71 | return nil 72 | default: 73 | return errInvalidPlaceName 74 | } 75 | } 76 | 77 | // UpdatePlace udpates your place's address. 78 | func (c *Client) UpdatePlace(pp *PlaceParams) (*Place, error) { 79 | if err := pp.Validate(); err != nil { 80 | return nil, err 81 | } 82 | 83 | blob, err := json.Marshal(&Place{Address: pp.Address}) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | fullURL := fmt.Sprintf("%s/places/%s", c.baseURL(), pp.Place) 89 | req, err := http.NewRequest("PUT", fullURL, bytes.NewReader(blob)) 90 | if err != nil { 91 | return nil, err 92 | } 93 | req.Header.Set("Content-Type", "application/json") 94 | return c.doPlaceReq(req) 95 | } 96 | -------------------------------------------------------------------------------- /v1/prices.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "net/http" 23 | "time" 24 | 25 | "github.com/orijtech/otils" 26 | ) 27 | 28 | type EstimateRequest struct { 29 | StartLatitude float64 `json:"start_latitude"` 30 | StartLongitude float64 `json:"start_longitude"` 31 | EndLongitude float64 `json:"end_longitude"` 32 | EndLatitude float64 `json:"end_latitude"` 33 | 34 | SeatCount int `json:"seat_count"` 35 | 36 | // ProductID is the UniqueID of the product 37 | // being requested. If unspecified, it will 38 | // default to the cheapest product for the 39 | // given location. 40 | ProductID string `json:"product_id"` 41 | 42 | StartPlace PlaceName `json:"start_place_id"` 43 | EndPlace PlaceName `json:"end_place_id"` 44 | 45 | Pager 46 | } 47 | 48 | type PriceEstimate struct { 49 | // ISO 4217 currency code. 50 | CurrencyCode otils.NullableString `json:"currency_code"` 51 | 52 | // Formatted string of estimate in local currency of the 53 | // start location. Estimate could be a range, a single 54 | // number(flat rate) or "Metered" for TAXI. 55 | Estimate otils.NullableString `json:"estimate"` 56 | 57 | // Expected activity duration in seconds. 58 | DurationSeconds otils.NullableFloat64 `json:"duration"` 59 | 60 | // Minimum price for product. 61 | MinimumPrice otils.NullableFloat64 `json:"minimum"` 62 | 63 | // Lower bound of the estimated price. 64 | LowEstimate otils.NullableFloat64 `json:"low_estimate"` 65 | 66 | // Upper bound of the estimated price. 67 | HighEstimate otils.NullableFloat64 `json:"high_estimate"` 68 | 69 | // Unique identifier representing a specific 70 | // product for a given longitude and latitude. 71 | // For example, uberX in San Francisco will have 72 | // a different ProductID than uberX in Los Angelese. 73 | ProductID string `json:"product_id"` 74 | 75 | // Display name of product. 76 | Name string `json:"display_name"` 77 | 78 | // Localized display name of product. 79 | LocalizedName string `json:"localized_display_name"` 80 | 81 | // Expected surge multiplier. Surge is active if 82 | // SurgeMultiplier is greater than 1. Price estimate 83 | // already factors in the surge multiplier. 84 | SurgeMultiplier otils.NullableFloat64 `json:"surge_multiplier"` 85 | 86 | LimitPerPage int64 `json:"limit"` 87 | } 88 | 89 | var errNilEstimateRequest = errors.New("expecting a non-nil estimateRequest") 90 | 91 | type PriceEstimatesPage struct { 92 | Estimates []*PriceEstimate `json:"prices"` 93 | 94 | Count int64 `json:"count,omitempty"` 95 | 96 | Err error 97 | PageNumber uint64 98 | } 99 | 100 | func (c *Client) EstimatePrice(ereq *EstimateRequest) (pagesChan chan *PriceEstimatesPage, cancelPaging func(), err error) { 101 | if ereq == nil { 102 | return nil, nil, errNilEstimateRequest 103 | } 104 | 105 | pager := new(Pager) 106 | if ereq != nil { 107 | *pager = ereq.Pager 108 | } 109 | 110 | // Adjust the paging parameters since they'll be heavily used 111 | pager.adjustPageParams() 112 | 113 | requestedMaxPage := pager.MaxPages 114 | pageNumberExceeds := func(pageNumber uint64) bool { 115 | if requestedMaxPage <= 0 { 116 | // No page limit requested at all here 117 | return false 118 | } 119 | 120 | return pageNumber >= uint64(requestedMaxPage) 121 | } 122 | 123 | cancelChan, cancelFn := makeCancelParadigm() 124 | estimatesPageChan := make(chan *PriceEstimatesPage) 125 | go func() { 126 | defer close(estimatesPageChan) 127 | 128 | throttleDuration := 150 * time.Millisecond 129 | pageNumber := uint64(0) 130 | 131 | canPage := true 132 | for canPage { 133 | ep := new(PriceEstimatesPage) 134 | ep.PageNumber = pageNumber 135 | 136 | qv, err := otils.ToURLValues(ereq) 137 | if err != nil { 138 | ep.Err = err 139 | estimatesPageChan <- ep 140 | return 141 | } 142 | 143 | fullURL := fmt.Sprintf("%s/estimates/price?%s", c.baseURL(), qv.Encode()) 144 | req, err := http.NewRequest("GET", fullURL, nil) 145 | if err != nil { 146 | ep.Err = err 147 | estimatesPageChan <- ep 148 | return 149 | } 150 | 151 | slurp, _, err := c.doReq(req) 152 | if err != nil { 153 | ep.Err = err 154 | estimatesPageChan <- ep 155 | return 156 | } 157 | 158 | if err := json.Unmarshal(slurp, ep); err != nil { 159 | ep.Err = err 160 | estimatesPageChan <- ep 161 | return 162 | } 163 | 164 | estimatesPageChan <- ep 165 | 166 | if ep.Count <= 0 { 167 | // No more items to page 168 | return 169 | } 170 | 171 | select { 172 | case <-cancelChan: 173 | // The user has canceled the paging 174 | canPage = false 175 | return 176 | 177 | case <-time.After(throttleDuration): 178 | // Do nothing here, the throttle time expired. 179 | } 180 | 181 | // Now it is time to adjust the next offset accordingly with the remaining count 182 | 183 | // Increment the page number as well 184 | pageNumber += 1 185 | if pageNumberExceeds(pageNumber) { 186 | return 187 | } 188 | 189 | pager.StartOffset += pager.LimitPerPage 190 | } 191 | }() 192 | 193 | return estimatesPageChan, cancelFn, nil 194 | } 195 | 196 | type FareEstimate struct { 197 | SurgeConfirmationURL string `json:"surge_confirmation_href,omitempty"` 198 | SurgeConfirmationID string `json:"surge_confirmation_id"` 199 | 200 | // Breakdown provides details on how a fare came to be. 201 | Breakdown []*FareBreakdown `json:"fare_breakdown,omitempty"` 202 | 203 | SurgeMultiplier otils.NullableFloat64 `json:"surge_multiplier"` 204 | 205 | CurrencyCode otils.NullableString `json:"currency_code"` 206 | DisplayAmount otils.NullableString `json:"display"` 207 | } 208 | 209 | type FareBreakdown struct { 210 | Low otils.NullableFloat64 `json:"low_amount,omitempty"` 211 | High otils.NullableFloat64 `json:"high_amount,omitempty"` 212 | DisplayAmount otils.NullableString `json:"display_amount,omitempty"` 213 | DisplayName otils.NullableString `json:"display_name,omitempty"` 214 | 215 | // Toll is the amount for tolls included in the fare. 216 | Toll otils.NullableFloat64 `json:"toll,omitempty"` 217 | 218 | // ServiceFee is the the fee collected by Uber for this fare. 219 | ServiceFee otils.NullableFloat64 `json:"service_fee,omitempty"` 220 | 221 | // Remainder is the remainder of the fare 222 | Remainder otils.NullableFloat64 `json:"other,omitempty"` 223 | } 224 | 225 | type Fare struct { 226 | Value otils.NullableFloat64 `json:"value,omitempty"` 227 | ExpiresAt int64 `json:"expires_at,omitempty"` 228 | CurrencyCode otils.NullableString `json:"currency_code"` 229 | DisplayAmount otils.NullableString `json:"display"` 230 | ID otils.NullableString `json:"fare_id"` 231 | } 232 | 233 | type UpfrontFare struct { 234 | Trip *Trip `json:"trip,omitempty"` 235 | Fare *Fare `json:"fare,omitempty"` 236 | 237 | // PickupEstimateMinutes is the estimated time of vehicle arrival 238 | // in minutes. It is unset if there are no cars available. 239 | PickupEstimateMinutes otils.NullableFloat64 `json:"pickup_estimate,omitempty"` 240 | 241 | Estimate *FareEstimate `json:"estimate,omitempty"` 242 | } 243 | 244 | func (upf *UpfrontFare) SurgeInEffect() bool { 245 | return upf != nil && upf.Estimate != nil && upf.Estimate.SurgeConfirmationURL != "" 246 | } 247 | 248 | func (upf *UpfrontFare) NoCarsAvailable() bool { 249 | return upf == nil || upf.PickupEstimateMinutes <= 0 250 | } 251 | 252 | var errInvalidSeatCount = errors.New("invalid seatcount, default and maximum value is 2") 253 | 254 | const defaultSeatCount = 2 255 | 256 | func (esReq *EstimateRequest) validateForUpfrontFare() error { 257 | if esReq == nil { 258 | return errNilEstimateRequest 259 | } 260 | 261 | // The number of seats required for uberPool. 262 | // Default and maximum value is 2. 263 | if esReq.SeatCount < 0 || esReq.SeatCount > 2 { 264 | return errInvalidSeatCount 265 | } 266 | 267 | if esReq.SeatCount == 0 { 268 | esReq.SeatCount = defaultSeatCount 269 | } 270 | 271 | // UpfrontFares require: 272 | // * StartPlace or (StartLatitude, StartLongitude) 273 | // * EndPlace or (EndLatitude, EndLongitude) 274 | if esReq.StartPlace != "" && esReq.EndPlace != "" { 275 | return nil 276 | } 277 | 278 | // However, checks for unspecified zero floats require 279 | // special attention, so we'll let the JSON marshaling 280 | // serialize them. 281 | return nil 282 | } 283 | 284 | var errNilFare = errors.New("failed to unmarshal the response fare") 285 | 286 | func (c *Client) UpfrontFare(esReq *EstimateRequest) (*UpfrontFare, error) { 287 | if err := esReq.validateForUpfrontFare(); err != nil { 288 | return nil, err 289 | } 290 | 291 | blob, err := json.Marshal(esReq) 292 | if err != nil { 293 | return nil, err 294 | } 295 | 296 | fullURL := fmt.Sprintf("%s/requests/estimate", c.baseURL()) 297 | req, err := http.NewRequest("POST", fullURL, bytes.NewReader(blob)) 298 | if err != nil { 299 | return nil, err 300 | } 301 | req.Header.Set("Content-Type", "application/json") 302 | slurp, _, err := c.doReq(req) 303 | if err != nil { 304 | return nil, err 305 | } 306 | 307 | upfrontFare := new(UpfrontFare) 308 | var blankUFare UpfrontFare 309 | if err := json.Unmarshal(slurp, upfrontFare); err != nil { 310 | return nil, err 311 | } 312 | if *upfrontFare == blankUFare { 313 | return nil, errNilFare 314 | } 315 | return upfrontFare, nil 316 | } 317 | -------------------------------------------------------------------------------- /v1/products.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "net/http" 22 | "reflect" 23 | "strings" 24 | 25 | "github.com/orijtech/otils" 26 | ) 27 | 28 | type Product struct { 29 | UpfrontFareEnabled bool `json:"upfront_fare_enabled,omitempty"` 30 | // Capacity is the number of people that can be 31 | // accomodated by the product for example, 4 people. 32 | Capacity int `json:"capacity,omitempty"` 33 | 34 | // The unique identifier representing a specific 35 | // product for a given latitude and longitude. 36 | // For example, uberX in San Francisco will have 37 | // a different ID than uberX in Los Angeles. 38 | ID string `json:"product_id"` 39 | 40 | // PriceDetails details the basic price 41 | // (not including any surge pricing adjustments). 42 | // This field is nil for products with metered 43 | // fares(taxi) or upfront fares(uberPOOL). 44 | PriceDetails *PriceDetails `json:"price_details"` 45 | 46 | ImageURL string `json:"image,omitempty"` 47 | CashEnabled bool `json:"cash_enabled,omitempty"` 48 | Shared bool `json:"shared"` 49 | 50 | // An abbreviated description of the product. 51 | // It is localized according to `Accept-Language` header. 52 | ShortDescription string `json:"short_description"` 53 | 54 | DisplayName string `json:"display_name"` 55 | 56 | Description string `json:"description"` 57 | } 58 | 59 | type ProductGroup string 60 | 61 | const ( 62 | ProductRideShare ProductGroup = "rideshare" 63 | ProductUberX ProductGroup = "uberx" 64 | ProductUberXL ProductGroup = "uberxl" 65 | ProductUberBlack ProductGroup = "uberblack" 66 | ProductSUV ProductGroup = "suv" 67 | ProductTaxi ProductGroup = "taxi" 68 | ) 69 | 70 | // ListProducts is a method that returns information about the 71 | // Uber products offered at a given location. 72 | // Some products such as uberEATS, are not returned by this 73 | // endpoint, at least as of: Fri 23 Jun 2017 18:01:04 MDT. 74 | // The results of this method do not reflect real-time availability 75 | // of the products. Please use the EstimateTime method to determine 76 | // real-time availability and ETAs of products. 77 | // In some markets, the list of products returned from this endpoint 78 | // may vary by the time of day due to time restrictions on 79 | // when that product may be utilized. 80 | func (c *Client) ListProducts(place *Place) ([]*Product, error) { 81 | qv, err := otils.ToURLValues(place) 82 | if err != nil { 83 | return nil, err 84 | } 85 | fullURL := fmt.Sprintf("%s/products?%s", c.baseURL(), qv.Encode()) 86 | req, err := http.NewRequest("GET", fullURL, nil) 87 | if err != nil { 88 | return nil, err 89 | } 90 | slurp, _, err := c.doReq(req) 91 | if err != nil { 92 | return nil, err 93 | } 94 | pWrap := new(productsWrap) 95 | if err := json.Unmarshal(slurp, pWrap); err != nil { 96 | return nil, err 97 | } 98 | return pWrap.Products, nil 99 | } 100 | 101 | var ( 102 | errEmptyProductID = errors.New("expecting a non-empty productID") 103 | errBlankProduct = errors.New("received a blank product back from the server") 104 | 105 | blankProductPtr = new(Product) 106 | ) 107 | 108 | func (c *Client) ProductByID(productID string) (*Product, error) { 109 | productID = strings.TrimSpace(productID) 110 | if productID == "" { 111 | return nil, errEmptyProductID 112 | } 113 | fullURL := fmt.Sprintf("%s/products/%s", c.baseURL(), productID) 114 | req, err := http.NewRequest("GET", fullURL, nil) 115 | if err != nil { 116 | return nil, err 117 | } 118 | slurp, _, err := c.doReq(req) 119 | if err != nil { 120 | return nil, err 121 | } 122 | product := new(Product) 123 | if err := json.Unmarshal(slurp, product); err != nil { 124 | return nil, err 125 | } 126 | if reflect.DeepEqual(product, blankProductPtr) { 127 | return nil, errBlankProduct 128 | } 129 | return product, nil 130 | } 131 | 132 | type productsWrap struct { 133 | Products []*Product `json:"products"` 134 | } 135 | 136 | type PriceDetails struct { 137 | // The base price of a trip. 138 | Base otils.NullableFloat64 `json:"base,omitempty"` 139 | 140 | // The minimum price of a trip. 141 | Minimum otils.NullableFloat64 `json:"minimum,omitempty"` 142 | 143 | // CostPerMinute is the charge per minute(if applicable for the product type). 144 | CostPerMinute otils.NullableFloat64 `json:"cost_per_minute,omitempty"` 145 | 146 | // CostPerDistanceUnit is the charge per 147 | // distance unit(if applicable for the product type). 148 | CostPerDistanceUnit otils.NullableFloat64 `json:"cost_per_distance,omitempty"` 149 | 150 | // DistanceUnit is the unit of distance used 151 | // to calculate the fare (either UnitMile or UnitKm) 152 | DistanceUnit Unit `json:"distance_unit,omitempty"` 153 | 154 | // Cancellation fee is what the rider has to pay after 155 | // they cancel the trip after the grace period. 156 | CancellationFee otils.NullableFloat64 `json:"cancellation_fee,omitempty"` 157 | 158 | CurrencyCode CurrencyCode `json:"currency_code,omitempty"` 159 | 160 | ServiceFees []*ServiceFee `json:"service_fees,omitempty"` 161 | } 162 | 163 | type ServiceFee struct { 164 | Name string `json:"name,omitempty"` 165 | 166 | Fee otils.NullableFloat64 `json:"fee,omitempty"` 167 | 168 | SplitFare otils.NullableFloat64 `json:"split_fare,omitempty"` 169 | } 170 | 171 | type Unit otils.NullableString 172 | 173 | const ( 174 | UnitMile otils.NullableString = "mile" 175 | UnitKM otils.NullableString = "km" 176 | ) 177 | -------------------------------------------------------------------------------- /v1/profile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "net/http" 23 | 24 | "github.com/orijtech/otils" 25 | ) 26 | 27 | type Profile struct { 28 | // First name of the Uber user. 29 | FirstName string `json:"first_name,omitempty"` 30 | 31 | // Last name of the Uber user. 32 | LastName string `json:"last_name,omitempty"` 33 | 34 | // Email address of the Uber user. 35 | Email string `json:"email,omitempty"` 36 | 37 | // Image URL of the Uber user. 38 | PictureURL string `json:"picture,omitempty"` 39 | 40 | // Whether the user has confirmed their mobile number. 41 | MobileVerified bool `json:"mobile_verified"` 42 | 43 | // The promotion code for the user. 44 | // Can be used for rewards when referring 45 | // other users to Uber. 46 | PromoCode string `json:"promo_code,omitempty"` 47 | 48 | ID string `json:"uuid,omitempty"` 49 | 50 | Rating otils.NullableFloat64 `json:"rating,omitempty"` 51 | 52 | ActivationStatus ActivationStatus `json:"activation_status,omitempty"` 53 | 54 | DriverID otils.NullableString `json:"driver_id,omitempty"` 55 | RiderID otils.NullableString `json:"rider_id,omitempty"` 56 | 57 | // Me if set, signifies that this Profile 58 | // is of current authenticated user. 59 | Me bool `json:"me,omitempty"` 60 | } 61 | 62 | func (c *Client) RetrieveMyProfile() (*Profile, error) { 63 | return c.retrieveProfile("/me") 64 | } 65 | 66 | func (c *Client) retrieveProfile(path string, versions ...string) (*Profile, error) { 67 | fullURL := fmt.Sprintf("%s%s", c.baseURL(versions...), path) 68 | req, err := http.NewRequest("GET", fullURL, nil) 69 | if err != nil { 70 | return nil, err 71 | } 72 | slurp, _, err := c.doReq(req) 73 | if err != nil { 74 | return nil, err 75 | } 76 | prof := new(Profile) 77 | if err := json.Unmarshal(slurp, prof); err != nil { 78 | return nil, err 79 | } 80 | return prof, nil 81 | } 82 | 83 | type PromoCode struct { 84 | Description string `json:"description,omitempty"` 85 | Code string `json:"promo_code,omitempty"` 86 | } 87 | 88 | var errNilPromoCode = errors.New("expecting a non-empty promoCode") 89 | 90 | type PromoCodeRequest struct { 91 | CodeToApply string `json:"applied_promotion_codes"` 92 | } 93 | 94 | func (c *Client) ApplyPromoCode(promoCode string) (*PromoCode, error) { 95 | if promoCode == "" { 96 | return nil, errNilPromoCode 97 | } 98 | 99 | pcReq := &PromoCodeRequest{ 100 | CodeToApply: promoCode, 101 | } 102 | 103 | blob, err := json.Marshal(pcReq) 104 | if err != nil { 105 | return nil, err 106 | } 107 | fullURL := fmt.Sprintf("%s/me", c.baseURL()) 108 | req, err := http.NewRequest("PATCH", fullURL, bytes.NewReader(blob)) 109 | if err != nil { 110 | return nil, err 111 | } 112 | req.Header.Set("Content-Type", "application/json") 113 | slurp, _, err := c.doReq(req) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | appliedPromoCode := new(PromoCode) 119 | if err := json.Unmarshal(slurp, appliedPromoCode); err != nil { 120 | return nil, err 121 | } 122 | 123 | return appliedPromoCode, nil 124 | } 125 | -------------------------------------------------------------------------------- /v1/receipts.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "net/http" 22 | 23 | "github.com/orijtech/otils" 24 | ) 25 | 26 | type Receipt struct { 27 | // Unique identifier representing Request. 28 | RequestID string `json:"request_id"` 29 | 30 | // Subtotal = TotalFare - ChargeAdjustments 31 | Subtotal otils.NullableString `json:"subtotal"` 32 | 33 | // The fare after credits and refunds have been applied. 34 | TotalFare otils.NullableString `json:"total_fare"` 35 | 36 | // The total amount charged to the user's payment method. 37 | // This is the subtotal (split if applicable) with taxes included. 38 | TotalCharged otils.NullableString `json:"total_charged"` 39 | 40 | // The total amount still owed after attempting to charge the 41 | // user. May be null if amount was paid in full. 42 | TotalOwed otils.NullableFloat64 `json:"total_owed"` 43 | 44 | // The ISO 4217 currency code of the amounts. 45 | CurrencyCode otils.NullableString `json:"currency_code"` 46 | 47 | // Duration is the ISO 8601 HH:MM:SS 48 | // format of the time duration of the trip. 49 | Duration otils.NullableString `json:"currency_code"` 50 | 51 | // Distance of the trip charged. 52 | Distance otils.NullableString `json:"distance"` 53 | 54 | // UnitOfDistance is the localized unit of distance. 55 | UnitOfDistance otils.NullableString `json:"distance_label"` 56 | } 57 | 58 | var errEmptyReceiptID = errors.New("expecting a non-empty receiptID") 59 | 60 | func (c *Client) RequestReceipt(receiptID string) (*Receipt, error) { 61 | if receiptID == "" { 62 | return nil, errEmptyReceiptID 63 | } 64 | 65 | fullURL := fmt.Sprintf("%s/requests/%s/receipt", c.baseURL(), receiptID) 66 | req, err := http.NewRequest("GET", fullURL, nil) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | slurp, _, err := c.doReq(req) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | receipt := new(Receipt) 77 | if err := json.Unmarshal(slurp, receipt); err != nil { 78 | return nil, err 79 | } 80 | 81 | return receipt, nil 82 | } 83 | -------------------------------------------------------------------------------- /v1/rides.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "net/http" 23 | "reflect" 24 | "strings" 25 | ) 26 | 27 | type RideRequest struct { 28 | // FareID is the ID of the upfront fare. If FareID is blank 29 | // and you would like an inspection of current estimates, 30 | // set PromptOnFare to review the upfront fare. 31 | FareID string `json:"fare_id,omitempty"` 32 | 33 | // PromptOnFare is an optional callback function that is 34 | // used when FareID is blank. It is invoked to inspect and 35 | // accept the upfront fare estimate or any surges in effect. 36 | PromptOnFare func(*UpfrontFare) error `json:"-"` 37 | 38 | // StartPlace can be used in place of (StartLatitude, StartLongitude) 39 | StartPlace PlaceName `json:"start_place_id,omitempty"` 40 | 41 | // EndPlace can be used in place of (EndLatitude, EndLongitude) 42 | EndPlace PlaceName `json:"end_place_id,omitempty"` 43 | 44 | StartLatitude float64 `json:"start_latitude,omitempty"` 45 | StartLongitude float64 `json:"start_longitude,omitempty"` 46 | EndLatitude float64 `json:"end_latitude,omitempty"` 47 | EndLongitude float64 `json:"end_longitude,omitempty"` 48 | 49 | // Optional fields 50 | // Product is the ID of the product being requested. If none is provided, 51 | // it will default to the cheapest product for the given location. 52 | ProductID string `json:"product_id,omitempty"` 53 | 54 | // SurgeConfirmationID is the unique identifier of the surge session for a user. 55 | // Required when returned from a 409 Conflict repsonse on a previous POST attempt. 56 | SurgeConfirmationID string `json:"surge_confirmation_id,omitempty"` 57 | 58 | // PaymentMethodID is the unique identifier of the payment method selected by a user. 59 | // If set, the trip will be requested using this payment method. If not set, the trip 60 | // will be requested using the user's last used payment method. 61 | PaymentMethodID string `json:"payment_method_id,omitempty"` 62 | 63 | // uberPOOL data 64 | // SeatCount is the number of seats required for uberPOOL. 65 | // The default and maximum value is 2. 66 | SeatCount int `json:"seat_count,omitempty"` 67 | 68 | // Uber for Business data 69 | // ExpenseCode is an alphanumeric identifier for expense reporting policies. 70 | // This value will appear in the trip receipt and any configured expense-reporting 71 | // integrations like: 72 | // * Uber For Business: https://www.uber.com/business 73 | // * Business Profiles: https://www.uber.com/business/profiles 74 | ExpenseCode string `json:"expense_code,omitempty"` 75 | 76 | // ExpenseMemo is a free text field to describe the purpose of the trip for 77 | // expense reporting. This value will appear in the trip receipt and any 78 | // configured expense-reporting integrations like: 79 | // * Uber For Business: https://www.uber.com/business 80 | // * Business Profiles: https://www.uber.com/business/profiles 81 | ExpenseMemo string `json:"expense_memo,omitempty"` 82 | } 83 | 84 | func (c *Client) preprocessBeforeValidate(rr *RideRequest) (*RideRequest, error) { 85 | if rr == nil || strings.TrimSpace(rr.FareID) != "" || rr.PromptOnFare == nil { 86 | return rr, nil 87 | } 88 | 89 | // Otherwise it is time to get the estimate of the fare 90 | upfrontFare, err := c.UpfrontFare(&EstimateRequest{ 91 | StartLatitude: rr.StartLatitude, 92 | StartLongitude: rr.StartLongitude, 93 | StartPlace: rr.StartPlace, 94 | EndPlace: rr.EndPlace, 95 | EndLatitude: rr.EndLatitude, 96 | EndLongitude: rr.EndLongitude, 97 | SeatCount: rr.SeatCount, 98 | }) 99 | 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | modRreq := new(RideRequest) 105 | // Shallow copy of the original then modify the copy. 106 | *modRreq = *rr 107 | modRreq.FareID = string(upfrontFare.Fare.ID) 108 | if modRreq.ProductID == "" { 109 | modRreq.ProductID = upfrontFare.Trip.ProductID 110 | } 111 | 112 | // Otherwise prompt for acceptance 113 | if err := rr.PromptOnFare(upfrontFare); err != nil { 114 | return nil, err 115 | } 116 | 117 | return modRreq, nil 118 | } 119 | 120 | func (rr *RideRequest) Validate() error { 121 | if rr == nil || strings.TrimSpace(rr.FareID) == "" { 122 | return ErrInvalidFareID 123 | } 124 | 125 | // Either: 126 | // 1. Start: 127 | // * StartPlace 128 | // * (StartLatitude, StartLongitude) 129 | // 2. End: 130 | // * EndPlace 131 | // * (EndLatitude, EndLongitude) 132 | if blankPlaceOrCoords(rr.StartPlace, rr.StartLatitude, rr.StartLongitude) { 133 | return ErrInvalidStartPlaceOrCoords 134 | } 135 | 136 | if blankPlaceOrCoords(rr.EndPlace, rr.EndLatitude, rr.EndLongitude) { 137 | return ErrInvalidEndPlaceOrCoords 138 | } 139 | 140 | return nil 141 | } 142 | 143 | func (c *Client) RequestRide(rreq *RideRequest) (*Ride, error) { 144 | rr, err := c.preprocessBeforeValidate(rreq) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | if err := rr.Validate(); err != nil { 150 | return nil, err 151 | } 152 | 153 | blob, err := json.Marshal(rr) 154 | if err != nil { 155 | return nil, err 156 | } 157 | fullURL := fmt.Sprintf("%s/requests", c.baseURL()) 158 | req, err := http.NewRequest("POST", fullURL, bytes.NewReader(blob)) 159 | if err != nil { 160 | return nil, err 161 | } 162 | req.Header.Set("Content-Type", "application/json") 163 | blob, _, err = c.doHTTPReq(req) 164 | if err != nil { 165 | return nil, err 166 | } 167 | ride := new(Ride) 168 | if err := json.Unmarshal(blob, ride); err != nil { 169 | return nil, err 170 | } 171 | return ride, nil 172 | } 173 | 174 | var ( 175 | ErrInvalidStartPlaceOrCoords = errors.New("invalid startPlace or (startLat, startLon)") 176 | ErrInvalidEndPlaceOrCoords = errors.New("invalid endPlace or (endLat, endLon)") 177 | ) 178 | 179 | func blankPlaceOrCoords(place PlaceName, lat, lon float64) bool { 180 | if strings.TrimSpace(string(place)) != "" { 181 | switch place { 182 | case PlaceHome, PlaceWork: 183 | return false 184 | default: 185 | return true 186 | } 187 | } 188 | 189 | // Otherwise now check out the coordinates 190 | // Coordinates can be any value 191 | return false 192 | } 193 | 194 | type Ride struct { 195 | RequestID string `json:"request_id,omitempty"` 196 | ProductID string `json:"product_id,omitempty"` 197 | 198 | // Status indicates the state of the ride request. 199 | Status Status `json:"status,omitempty"` 200 | Shared bool `json:"shared,omitempty"` 201 | 202 | Vehicle *Vehicle `json:"vehicle,omitempty"` 203 | Driver *Driver `json:"driver,omitempty"` 204 | Location *Location `json:"location,omitempty"` 205 | 206 | Pickup *Location `json:"pickup,omitempty"` 207 | Destination *Location `json:"destination,omitempty"` 208 | 209 | // ETAMinutes is the expected time of arrival in minutes. 210 | ETAMinutes int `json:"eta,omitempty"` 211 | 212 | // The surge pricing multiplier used to calculate the increased price of a request. 213 | // A surge multiplier of 1.0 means surge pricing is not in effect. 214 | SurgeMultiplier float32 `json:"surge_multiplier,omitempty"` 215 | } 216 | 217 | func (r *Ride) SurgeInEffect() bool { 218 | return r != nil && r.SurgeMultiplier == 1.0 219 | } 220 | 221 | type Vehicle struct { 222 | Model string `json:"model"` 223 | Make string `json:"make"` 224 | 225 | LicensePlate string `json:"license_plate"` 226 | PictureURL string `json:"picture_url"` 227 | } 228 | 229 | type Driver struct { 230 | PhoneNumber string `json:"phone_number"` 231 | SMSNumber string `json:"sms_number"` 232 | 233 | PictureURL string `json:"picture_url"` 234 | Name string `json:"name"` 235 | Rating int `json:"rating"` 236 | } 237 | 238 | type State string 239 | 240 | type Location struct { 241 | Latitude float64 `json:"latitude,omitempty"` 242 | Longitude float64 `json:"longitude,omitempty"` 243 | 244 | // Bearing is the current bearing of the vehicle in degrees (0-359). 245 | Bearing int `json:"bearing,omitempty"` 246 | 247 | PrimaryAddress string `json:"address,omitempty"` 248 | SecondaryAddress string `json:"address_2,omitempty"` 249 | City string `json:"city,omitempty"` 250 | State string `json:"state,omitempty"` 251 | PostalCode string `json:"postal_code,omitempty"` 252 | Country string `json:"country,omitempty"` 253 | 254 | ETAMinutes float32 `json:"eta,omitempty"` 255 | 256 | // WaypointType is used to describe Waypoints. 257 | // Its values could be: 258 | // + pickup 259 | // + dropoff 260 | WaypointType WaypointType `json:"type,omitempty"` 261 | } 262 | 263 | type WaypointType string 264 | 265 | const ( 266 | PickupWaypoint WaypointType = "pickup" 267 | Dropoffpoint WaypointType = "dropoff" 268 | ) 269 | 270 | var blankTrip = new(Trip) 271 | var errBlankTrip = errors.New("expecting a non-blank trip") 272 | 273 | // CurrentTrip returns the details of the ongoing trip. 274 | // It is a privileged method that requires FULL ACCESS when 275 | // used for all Uber riders. See more information about scopes 276 | // here https://developer.uber.com/docs/riders/guides/scopes. 277 | func (c *Client) CurrentTrip() (*Trip, error) { 278 | tripURL := fmt.Sprintf("%s/requests/current", c.baseURL()) 279 | return c.fetchTripByURL(tripURL) 280 | } 281 | 282 | // TripByID returns the details of a trip whose ID is known. 283 | // It is a privileged method that requires FULL ACCESS when 284 | // used for all Uber riders. See more information about scopes 285 | // here https://developer.uber.com/docs/riders/guides/scopes. 286 | func (c *Client) TripByID(id string) (*Trip, error) { 287 | tripURL := fmt.Sprintf("%s/requests/%s", c.baseURL(), id) 288 | return c.fetchTripByURL(tripURL) 289 | } 290 | 291 | func (c *Client) fetchTripByURL(tripURL string) (*Trip, error) { 292 | req, err := http.NewRequest("GET", tripURL, nil) 293 | if err != nil { 294 | return nil, err 295 | } 296 | blob, _, err := c.doAuthAndHTTPReq(req) 297 | if err != nil { 298 | return nil, err 299 | } 300 | 301 | tr := new(Trip) 302 | if err := json.Unmarshal(blob, tr); err != nil { 303 | return nil, err 304 | } 305 | if reflect.DeepEqual(tr, blankTrip) { 306 | return nil, errBlankTrip 307 | } 308 | return tr, nil 309 | } 310 | -------------------------------------------------------------------------------- /v1/status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | type Status string 18 | 19 | const ( 20 | // The request is matching to 21 | // the most efficient available driver. 22 | StatusProcessing Status = "processing" 23 | 24 | // The request was unfulfilled because 25 | // no drivers were available. 26 | StatusNoDriversAvailable Status = "no_drivers_available" 27 | 28 | // The request has been accepted by a driver and 29 | // is "en route" to the start location 30 | // (i.e. start_latitude and start_longitude). 31 | // This state can occur multiple times in case of 32 | // a driver re-assignment. 33 | StatusAccepted Status = "accepted" 34 | 35 | // The driver has arrived or will be shortly. 36 | StatusArriving Status = "arriving" 37 | 38 | // The request is "en route" from the 39 | // start location to the end location. 40 | StatusInProgress Status = "in_progress" 41 | 42 | // The request has been canceled by the driver. 43 | StatusDriverCanceled Status = "driver_canceled" 44 | 45 | // The request has been canceled by the rider. 46 | StatusRiderCanceled Status = "rider_canceled" 47 | 48 | // The request has been completed by the driver. 49 | StatusCompleted Status = "completed" 50 | 51 | // The receipt for the trip is ready. 52 | StatusReceiptReady Status = "ready" 53 | ) 54 | -------------------------------------------------------------------------------- /v1/testdata/delivery-gizmo.json: -------------------------------------------------------------------------------- 1 | { 2 | "courier": null, 3 | "created_at": 1441146983, 4 | "currency_code": "USD", 5 | "delivery_id": "b32d5374-7cee-4bc0-b588-f3820ab9b98c", 6 | "dropoff": { 7 | "contact": { 8 | "company_name": "Gizmo Shop", 9 | "email": "contact@uber.com", 10 | "first_name": "Calvin", 11 | "last_name": "Lee", 12 | "phone": { 13 | "number": "+14081234567", 14 | "sms_enabled": false 15 | }, 16 | "send_email_notifications": true, 17 | "send_sms_notifications": true 18 | }, 19 | "eta": 20, 20 | "location": { 21 | "address": "530 W 113th Street", 22 | "address_2": "Floor 2", 23 | "city": "New York", 24 | "country": "US", 25 | "postal_code": "10025", 26 | "state": "NY" 27 | }, 28 | "signature_required": false, 29 | "special_instructions": null 30 | }, 31 | "fee": 5.0, 32 | "items": [ 33 | { 34 | "height": 5.0, 35 | "is_fragile": false, 36 | "length": 14.5, 37 | "price": 1.0, 38 | "quantity": 1, 39 | "title": "Shoes", 40 | "weight": 2.0, 41 | "width": 7.0 42 | }, 43 | { 44 | "height": 5.0, 45 | "is_fragile": false, 46 | "length": 25.0, 47 | "quantity": 1, 48 | "title": "Guitar", 49 | "weight": 10.0, 50 | "width": 12.0 51 | } 52 | ], 53 | "order_reference_id": "SDA124KA", 54 | "pickup": { 55 | "contact": { 56 | "company_name": "Gizmo Shop", 57 | "email": "contact@uber.com", 58 | "first_name": "Calvin", 59 | "last_name": "Lee", 60 | "phone": { 61 | "number": "+14081234567", 62 | "sms_enabled": false 63 | }, 64 | "send_email_notifications": true, 65 | "send_sms_notifications": true 66 | }, 67 | "eta": 5, 68 | "location": { 69 | "address": "636 W 28th Street", 70 | "address_2": "Floor 2", 71 | "city": "New York", 72 | "country": "US", 73 | "postal_code": "10001", 74 | "state": "NY" 75 | }, 76 | "special_instructions": "Go to pickup counter in back of shop." 77 | }, 78 | "quote_id": "KEBjNGUxNjhlZmNmMDA4ZGJjNmJlY2EwOGJlN2M0ZjdmZjI2Y2VkZDdmMmQ2MDJlZDJjMTc4MzM2ODU2YzRkMzU4FYihsd4KFbiqsd4KFYD1sgwcFdD/0oQDFYfw48EFABwVyoCThQMVp/qvwQUAGANVU0QA", 79 | "status": "processing", 80 | "tracking_url": null, 81 | "batch": { 82 | "batch_id": "963233d3-e8ad-4ed9-aae7-95446ffee22f", 83 | "count": 2, 84 | "deliveries": ["8b58bc58-7352-4278-b569-b5d24d8e3f76", "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /v1/testdata/driverProfile-TEST_TOKEN-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 3 | "first_name": "Uber", 4 | "last_name": "Tester", 5 | "email": "uber.developer+tester@example.com", 6 | "phone_number": "+15555550001", 7 | "picture": "https://d1w2poirtb3as9.cloudfront.net/16ce502f4767f17b120e.png", 8 | "promo_code": "ubert4544ue", 9 | "rating": 5, 10 | "activation_status": "active" 11 | } -------------------------------------------------------------------------------- /v1/testdata/driver_payments_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 1200, 3 | "limit": 2, 4 | "payments": [ 5 | { 6 | "payment_id": "5cb8304c-f3f0-4a46-b6e3-b55e020750d7", 7 | "category": "fare", 8 | "event_time": 1502842757, 9 | "trip_id": "5cb8304c-f3f0-4a46-b6e3-b55e020750d7", 10 | "cash_collected": 0, 11 | "amount": 3.12, 12 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 13 | "breakdown": { 14 | "other": 4.16, 15 | "toll": 1, 16 | "service_fee": -1.04 17 | }, 18 | "rider_fees": { 19 | "split_fare": 0.50 20 | }, 21 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 22 | "currency_code": "USD" 23 | }, 24 | { 25 | "payment_id": "135832d7-e3ff-400d-a8d6-055509704cc1", 26 | "category": "fare", 27 | "event_time": 1502842852, 28 | "trip_id": "4ff32d9e-ac5e-4008-912d-176bd5a28d9e", 29 | "cash_collected": 0.89, 30 | "amount": 13.12, 31 | "driver_id": "7PvWuMAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aQpXIYih--U83Eh7LEBiksj2xyhB==", 32 | "breakdown": { 33 | "other": 0.16, 34 | "toll": 2, 35 | "service_fee": 51.04 36 | }, 37 | "rider_fees": { 38 | "split_fare": 0.50 39 | }, 40 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 41 | "currency_code": "USD" 42 | } 43 | ], 44 | "offset": 0 45 | } 46 | -------------------------------------------------------------------------------- /v1/testdata/driver_payments_10.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /v1/testdata/driver_payments_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 1200, 3 | "limit": 2, 4 | "payments": [ 5 | { 6 | "payment_id": "c23995e3-1487-4d08-a198-bea64a5ba8a7", 7 | "category": "fare", 8 | "event_time": 1501862352, 9 | "trip_id": "07afcb27-ea8b-4c42-bbb5-97638fdc6f41", 10 | "cash_collected": 10, 11 | "amount": 0, 12 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 13 | "breakdown": { 14 | "other": 4.16, 15 | "toll": 1, 16 | "service_fee": -1.04 17 | }, 18 | "rider_fees": { 19 | "split_fare": 0.50 20 | }, 21 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 22 | "currency_code": "USD" 23 | }, 24 | { 25 | "payment_id": "135832d7-e3ff-400d-a8d6-055509704cc1", 26 | "category": "fare", 27 | "event_time": 1502842852, 28 | "trip_id": "4ff32d9e-ac5e-4008-912d-176bd5a28d9e", 29 | "cash_collected": 0.89, 30 | "amount": 13.12, 31 | "driver_id": "7PvWuMAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aQpXIYih--U83Eh7LEBiksj2xyhB==", 32 | "breakdown": { 33 | "other": 0.16, 34 | "toll": 2, 35 | "service_fee": 51.04 36 | }, 37 | "rider_fees": { 38 | "split_fare": 0.50 39 | }, 40 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 41 | "currency_code": "USD" 42 | } 43 | ], 44 | "offset": 2 45 | } 46 | -------------------------------------------------------------------------------- /v1/testdata/driver_payments_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 1200, 3 | "limit": 2, 4 | "payments": [ 5 | { 6 | "payment_id": "c23995e3-1487-4d08-a198-bea64a5ba8a7", 7 | "category": "fare", 8 | "event_time": 1501862352, 9 | "trip_id": "07afcb27-ea8b-4c42-bbb5-97638fdc6f41", 10 | "cash_collected": 10, 11 | "amount": 0, 12 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 13 | "breakdown": { 14 | "other": 4.16, 15 | "toll": 1, 16 | "service_fee": -1.04 17 | }, 18 | "rider_fees": { 19 | "split_fare": 0.50 20 | }, 21 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 22 | "currency_code": "USD" 23 | }, 24 | { 25 | "payment_id": "135832d7-e3ff-400d-a8d6-055509704cc1", 26 | "category": "fare", 27 | "event_time": 1502842852, 28 | "trip_id": "4ff32d9e-ac5e-4008-912d-176bd5a28d9e", 29 | "cash_collected": 0.89, 30 | "amount": 13.12, 31 | "driver_id": "7PvWuMAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aQpXIYih--U83Eh7LEBiksj2xyhB==", 32 | "breakdown": { 33 | "other": 0.16, 34 | "toll": 2, 35 | "service_fee": 51.04 36 | }, 37 | "rider_fees": { 38 | "split_fare": 0.50 39 | }, 40 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 41 | "currency_code": "USD" 42 | } 43 | ], 44 | "offset": 4 45 | } 46 | -------------------------------------------------------------------------------- /v1/testdata/driver_payments_6.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 1200, 3 | "limit": 4, 4 | "payments": [ 5 | { 6 | "payment_id": "1638dd86-a690-4903-8cae-d3b0b1566b91", 7 | "category": "fare", 8 | "event_time": 1501862352, 9 | "trip_id": "1638dd86-a690-4903-8cae-d3b0b1566b91", 10 | "cash_collected": 0, 11 | "amount": 0.16, 12 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 13 | "breakdown": { 14 | "other": 7.76, 15 | "toll": 1, 16 | "service_fee": 1.04 17 | }, 18 | "rider_fees": { 19 | "split_fare": 3.45 20 | }, 21 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 22 | "currency_code": "CAD" 23 | }, 24 | { 25 | "payment_id": "b98cc052-ff36-4fb0-8b59-b7f104f0b412", 26 | "category": "fare", 27 | "event_time": 1502842852, 28 | "trip_id": "b98cc052-ff36-4fb0-8b59-b7f104f0b412", 29 | "cash_collected": 0.89, 30 | "amount": 13.12, 31 | "driver_id": "7PvWuMAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aQpXIYih--U83Eh7LEBiksj2xyhB==", 32 | "breakdown": { 33 | "other": 9.16, 34 | "toll": 1, 35 | "service_fee": 2.11 36 | }, 37 | "rider_fees": { 38 | "split_fare": 3.37 39 | }, 40 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 41 | "currency_code": "CTH" 42 | }, 43 | { 44 | "payment_id": "8e367a25-7fe2-4430-953e-54ad2dd94f3e", 45 | "category": "fare", 46 | "event_time": 1502862461, 47 | "trip_id": "8e367a25-7fe2-4430-953e-54ad2dd94f3e", 48 | "cash_collected": 10, 49 | "amount": 1.26, 50 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 51 | "breakdown": { 52 | "other": 7.76, 53 | "toll": 1, 54 | "service_fee": 1.04 55 | }, 56 | "rider_fees": { 57 | "split_fare": 3.45 58 | }, 59 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 60 | "currency_code": "USD" 61 | }, 62 | { 63 | "payment_id": "62bcf820-cad0-451d-b991-86b75dc7f1e1", 64 | "category": "fare", 65 | "event_time": 1502865911, 66 | "trip_id": "62bcf820-cad0-451d-b991-86b75dc7f1e1", 67 | "cash_collected": 10.89, 68 | "amount": 3.8, 69 | "driver_id": "7PvWuMAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aQpXIYih--U83Eh7LEBiksj2xyhB==", 70 | "breakdown": { 71 | "other": 4.03, 72 | "toll": 1, 73 | "service_fee": 25.16 74 | }, 75 | "rider_fees": { 76 | "split_fare": 2.28 77 | }, 78 | "partner_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 79 | "currency_code": "USD" 80 | } 81 | ], 82 | "offset": 6 83 | } 84 | -------------------------------------------------------------------------------- /v1/testdata/driver_trips_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 1200, 3 | "limit": 2, 4 | "trips": [ 5 | { 6 | "fare": 6.2, 7 | "dropoff": { 8 | "timestamp": 1502844378 9 | }, 10 | "vehicle_id": "0082b54a-6a5e-4f6b-b999-b0649f286381", 11 | "distance": 0.37, 12 | "start_city": { 13 | "latitude": 38.3498, 14 | "display_name": "Charleston, WV", 15 | "longitude": -81.6326 16 | }, 17 | "status_changes": [ 18 | { 19 | "status": "accepted", 20 | "timestamp": 1502843899 21 | }, 22 | { 23 | "status": "driver_arrived", 24 | "timestamp": 1502843900 25 | }, 26 | { 27 | "status": "trip_began", 28 | "timestamp": 1502843903 29 | }, 30 | { 31 | "status": "completed", 32 | "timestamp": 1502844378 33 | } 34 | ], 35 | "surge_multiplier": 1, 36 | "pickup": { 37 | "timestamp": 1502843903 38 | }, 39 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 40 | "status": "completed", 41 | "duration": 475, 42 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 43 | "currency_code": "USD" 44 | }, 45 | { 46 | "fare": 8.2, 47 | "dropoff": { 48 | "timestamp": 1502846443 49 | }, 50 | "vehicle_id": "f227de83-0f6a-4422-a733-1e8b781b6ff7", 51 | "distance": 2.11, 52 | "start_city": { 53 | "latitude": 38.3498, 54 | "display_name": "Charleston, WV", 55 | "longitude": -81.6326 56 | }, 57 | "status_changes": [ 58 | { 59 | "status": "accepted", 60 | "timestamp": 1502844744 61 | }, 62 | { 63 | "status": "driver_arrived", 64 | "timestamp": 1502843900 65 | }, 66 | { 67 | "status": "trip_began", 68 | "timestamp": 1502843903 69 | }, 70 | { 71 | "status": "completed", 72 | "timestamp": 1502844378 73 | } 74 | ], 75 | "surge_multiplier": 1.93, 76 | "pickup": { 77 | "timestamp": 1502843903 78 | }, 79 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 80 | "status": "completed", 81 | "duration": 475, 82 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 83 | "currency_code": "USD" 84 | } 85 | ], 86 | "offset": 0 87 | } 88 | -------------------------------------------------------------------------------- /v1/testdata/driver_trips_10.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /v1/testdata/driver_trips_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 1200, 3 | "limit": 2, 4 | "trips": [ 5 | { 6 | "fare": 6.2, 7 | "dropoff": { 8 | "timestamp": 1502844378 9 | }, 10 | "vehicle_id": "0082b54a-6a5e-4f6b-b999-b0649f286381", 11 | "distance": 0.37, 12 | "start_city": { 13 | "latitude": 38.3498, 14 | "display_name": "Charleston, WV", 15 | "longitude": -81.6326 16 | }, 17 | "status_changes": [ 18 | { 19 | "status": "accepted", 20 | "timestamp": 1502843899 21 | }, 22 | { 23 | "status": "driver_arrived", 24 | "timestamp": 1502843900 25 | }, 26 | { 27 | "status": "trip_began", 28 | "timestamp": 1502843903 29 | }, 30 | { 31 | "status": "completed", 32 | "timestamp": 1502844378 33 | } 34 | ], 35 | "surge_multiplier": 1, 36 | "pickup": { 37 | "timestamp": 1502843903 38 | }, 39 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 40 | "status": "completed", 41 | "duration": 475, 42 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 43 | "currency_code": "USD" 44 | }, 45 | { 46 | "fare": 8.2, 47 | "dropoff": { 48 | "timestamp": 1502846443 49 | }, 50 | "vehicle_id": "f227de83-0f6a-4422-a733-1e8b781b6ff7", 51 | "distance": 2.11, 52 | "start_city": { 53 | "latitude": 38.3498, 54 | "display_name": "Charleston, WV", 55 | "longitude": -81.6326 56 | }, 57 | "status_changes": [ 58 | { 59 | "status": "accepted", 60 | "timestamp": 1502844744 61 | }, 62 | { 63 | "status": "driver_arrived", 64 | "timestamp": 1502843900 65 | }, 66 | { 67 | "status": "trip_began", 68 | "timestamp": 1502843903 69 | }, 70 | { 71 | "status": "completed", 72 | "timestamp": 1502844378 73 | } 74 | ], 75 | "surge_multiplier": 1.93, 76 | "pickup": { 77 | "timestamp": 1502843903 78 | }, 79 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 80 | "status": "completed", 81 | "duration": 475, 82 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 83 | "currency_code": "USD" 84 | } 85 | ], 86 | "offset": 2 87 | } 88 | -------------------------------------------------------------------------------- /v1/testdata/driver_trips_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 1200, 3 | "limit": 2, 4 | "trips": [ 5 | { 6 | "fare": 6.2, 7 | "dropoff": { 8 | "timestamp": 1502844378 9 | }, 10 | "vehicle_id": "0082b54a-6a5e-4f6b-b999-b0649f286381", 11 | "distance": 0.37, 12 | "start_city": { 13 | "latitude": 38.3498, 14 | "display_name": "Charleston, WV", 15 | "longitude": -81.6326 16 | }, 17 | "status_changes": [ 18 | { 19 | "status": "accepted", 20 | "timestamp": 1502843899 21 | }, 22 | { 23 | "status": "driver_arrived", 24 | "timestamp": 1502843900 25 | }, 26 | { 27 | "status": "trip_began", 28 | "timestamp": 1502843903 29 | }, 30 | { 31 | "status": "completed", 32 | "timestamp": 1502844378 33 | } 34 | ], 35 | "surge_multiplier": 1, 36 | "pickup": { 37 | "timestamp": 1502843903 38 | }, 39 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 40 | "status": "completed", 41 | "duration": 475, 42 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 43 | "currency_code": "USD" 44 | }, 45 | { 46 | "fare": 8.2, 47 | "dropoff": { 48 | "timestamp": 1502846443 49 | }, 50 | "vehicle_id": "f227de83-0f6a-4422-a733-1e8b781b6ff7", 51 | "distance": 2.11, 52 | "start_city": { 53 | "latitude": 38.3498, 54 | "display_name": "Charleston, WV", 55 | "longitude": -81.6326 56 | }, 57 | "status_changes": [ 58 | { 59 | "status": "accepted", 60 | "timestamp": 1502844744 61 | }, 62 | { 63 | "status": "driver_arrived", 64 | "timestamp": 1502843900 65 | }, 66 | { 67 | "status": "trip_began", 68 | "timestamp": 1502843903 69 | }, 70 | { 71 | "status": "completed", 72 | "timestamp": 1502844378 73 | } 74 | ], 75 | "surge_multiplier": 1.93, 76 | "pickup": { 77 | "timestamp": 1502843903 78 | }, 79 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 80 | "status": "completed", 81 | "duration": 475, 82 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 83 | "currency_code": "USD" 84 | } 85 | ], 86 | "offset": 4 87 | } 88 | -------------------------------------------------------------------------------- /v1/testdata/driver_trips_6.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 1200, 3 | "limit": 4, 4 | "trips": [ 5 | { 6 | "fare": 6.2, 7 | "dropoff": { 8 | "timestamp": 1502844378 9 | }, 10 | "vehicle_id": "0082b54a-6a5e-4f6b-b999-b0649f286381", 11 | "distance": 0.37, 12 | "start_city": { 13 | "latitude": 38.3498, 14 | "display_name": "Charleston, WV", 15 | "longitude": -81.6326 16 | }, 17 | "status_changes": [ 18 | { 19 | "status": "accepted", 20 | "timestamp": 1502843899 21 | }, 22 | { 23 | "status": "driver_arrived", 24 | "timestamp": 1502843900 25 | }, 26 | { 27 | "status": "trip_began", 28 | "timestamp": 1502843903 29 | }, 30 | { 31 | "status": "completed", 32 | "timestamp": 1502844378 33 | } 34 | ], 35 | "surge_multiplier": 1, 36 | "pickup": { 37 | "timestamp": 1502843903 38 | }, 39 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 40 | "status": "completed", 41 | "duration": 475, 42 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 43 | "currency_code": "USD" 44 | }, 45 | { 46 | "fare": 8.2, 47 | "dropoff": { 48 | "timestamp": 1502846443 49 | }, 50 | "vehicle_id": "f227de83-0f6a-4422-a733-1e8b781b6ff7", 51 | "distance": 2.11, 52 | "start_city": { 53 | "latitude": 38.3498, 54 | "display_name": "Charleston, WV", 55 | "longitude": -81.6326 56 | }, 57 | "status_changes": [ 58 | { 59 | "status": "accepted", 60 | "timestamp": 1502844744 61 | }, 62 | { 63 | "status": "driver_arrived", 64 | "timestamp": 1502843900 65 | }, 66 | { 67 | "status": "trip_began", 68 | "timestamp": 1502843903 69 | }, 70 | { 71 | "status": "completed", 72 | "timestamp": 1502844378 73 | } 74 | ], 75 | "surge_multiplier": 1.93, 76 | "pickup": { 77 | "timestamp": 1502843903 78 | }, 79 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 80 | "status": "completed", 81 | "duration": 475, 82 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 83 | "currency_code": "USD" 84 | }, 85 | { 86 | "fare": 6.2, 87 | "dropoff": { 88 | "timestamp": 1502844378 89 | }, 90 | "vehicle_id": "0082b54a-6a5e-4f6b-b999-b0649f286381", 91 | "distance": 0.37, 92 | "start_city": { 93 | "latitude": 38.3498, 94 | "display_name": "Charleston, WV", 95 | "longitude": -81.6326 96 | }, 97 | "status_changes": [ 98 | { 99 | "status": "accepted", 100 | "timestamp": 1502843899 101 | }, 102 | { 103 | "status": "driver_arrived", 104 | "timestamp": 1502843900 105 | }, 106 | { 107 | "status": "trip_began", 108 | "timestamp": 1502843903 109 | }, 110 | { 111 | "status": "completed", 112 | "timestamp": 1502844378 113 | } 114 | ], 115 | "surge_multiplier": 1, 116 | "pickup": { 117 | "timestamp": 1502843903 118 | }, 119 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 120 | "status": "completed", 121 | "duration": 475, 122 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 123 | "currency_code": "USD" 124 | }, 125 | { 126 | "fare": 8.2, 127 | "dropoff": { 128 | "timestamp": 1502846443 129 | }, 130 | "vehicle_id": "f227de83-0f6a-4422-a733-1e8b781b6ff7", 131 | "distance": 2.11, 132 | "start_city": { 133 | "latitude": 38.3498, 134 | "display_name": "Charleston, WV", 135 | "longitude": -81.6326 136 | }, 137 | "status_changes": [ 138 | { 139 | "status": "accepted", 140 | "timestamp": 1502844744 141 | }, 142 | { 143 | "status": "driver_arrived", 144 | "timestamp": 1502843900 145 | }, 146 | { 147 | "status": "trip_began", 148 | "timestamp": 1502843903 149 | }, 150 | { 151 | "status": "completed", 152 | "timestamp": 1502844378 153 | } 154 | ], 155 | "surge_multiplier": 1.93, 156 | "pickup": { 157 | "timestamp": 1502843903 158 | }, 159 | "driver_id": "8LvWuRAq2511gmr8EMkovekFNa2848lyMaQevIto-aXmnK9oKNRtfTxYLgPq9OSt8EzAu5pDB7XiaQIrcp-zXgOA5EyK4h00U6D1o7aZpXIQah--U77Eh7LEBiksj2rahB==", 160 | "status": "completed", 161 | "duration": 475, 162 | "trip_id": "b5613b6a-fe74-4704-a637-50f8d51a8bb1", 163 | "currency_code": "USD" 164 | } 165 | ], 166 | "offset": 6 167 | } 168 | -------------------------------------------------------------------------------- /v1/testdata/fare-estimate-no-surge.json: -------------------------------------------------------------------------------- 1 | { 2 | "fare": { 3 | "value": 5.73, 4 | "fare_id": "d30e732b8bba22c9cdc10513ee86380087cb4a6f89e37ad21ba2a39f3a1ba960", 5 | "expires_at": 1476953293, 6 | "display": "$5.73", 7 | "currency_code": "USD" 8 | }, 9 | "trip": { 10 | "distance_unit": "mile", 11 | "duration_estimate": 540, 12 | "distance_estimate": 2.39 13 | }, 14 | "pickup_estimate": 2 15 | } -------------------------------------------------------------------------------- /v1/testdata/fare-estimate-surge.json: -------------------------------------------------------------------------------- 1 | { 2 | "estimate": { 3 | "surge_confirmation_href": "https:\/\/api.uber.com\/v1\/surge-confirmations\/7d604f5e", 4 | "high_estimate": 11, 5 | "surge_confirmation_id": "7d604f5e", 6 | "minimum": 5, 7 | "low_estimate": 8, 8 | "fare_breakdown": [ 9 | { 10 | "low_amount": 1.25, 11 | "high_amount": 1.25, 12 | "display_amount": "1.25", 13 | "display_name": "Base Fare" 14 | }, 15 | { 16 | "low_amount": 1.92, 17 | "high_amount": 2.57, 18 | "display_amount": "1.92-2.57", 19 | "display_name": "Distance" 20 | }, 21 | { 22 | "low_amount": 2.50, 23 | "high_amount": 3.50, 24 | "display_amount": "2.50-3.50", 25 | "display_name": "Surge x1.5" 26 | }, 27 | { 28 | "low_amount": 1.25, 29 | "high_amount": 1.25, 30 | "display_amount": "1.25", 31 | "display_name": "Booking Fee" 32 | }, 33 | { 34 | "low_amount": 1.36, 35 | "high_amount": 1.81, 36 | "display_amount": "1.36-1.81", 37 | "display_name": "Time" 38 | } 39 | ], 40 | "surge_multiplier": 1.5, 41 | "display": "$8-11", 42 | "currency_code": "USD" 43 | }, 44 | "trip": { 45 | "distance_unit": "mile", 46 | "duration_estimate": 480, 47 | "distance_estimate": 1.95 48 | }, 49 | "pickup_estimate": 2 50 | } -------------------------------------------------------------------------------- /v1/testdata/list-payments-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "payment_methods": [ 3 | { 4 | "payment_method_id": "5f384f7d-8323-4207-a297-51c571234a8c", 5 | "type": "baidu_wallet", 6 | "description": "***53" 7 | }, 8 | { 9 | "payment_method_id": "f33847de-8113-4587-c307-51c2d13a823c", 10 | "type": "alipay", 11 | "description": "ga***@uber.com" 12 | }, 13 | { 14 | "payment_method_id": "f43847de-8113-4587-c307-51c2d13a823c", 15 | "type": "visa", 16 | "description": "***23" 17 | }, 18 | { 19 | "payment_method_id": "517a6c29-3a2b-45cb-94a3-35d679909a71", 20 | "type": "american_express", 21 | "description": "***05" 22 | }, 23 | { 24 | "payment_method_id": "f53847de-8113-4587-c307-51c2d13a823c", 25 | "type": "business_account", 26 | "description": "Late Night Ride" 27 | } 28 | ], 29 | "last_used": "f53847de-8113-4587-c307-51c2d13a823c" 30 | } 31 | -------------------------------------------------------------------------------- /v1/testdata/listProducts.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "upfront_fare_enabled": true, 5 | "capacity": 2, 6 | "product_id": "26546650-e557-4a7b-86e7-6a3942445247", 7 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", 8 | "cash_enabled": false, 9 | "shared": true, 10 | "short_description": "POOL", 11 | "display_name": "POOL", 12 | "product_group": "rideshare", 13 | "description": "Share the ride, split the cost." 14 | }, 15 | { 16 | "upfront_fare_enabled": true, 17 | "capacity": 4, 18 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 19 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", 20 | "cash_enabled": false, 21 | "shared": false, 22 | "short_description": "uberX", 23 | "display_name": "uberX", 24 | "product_group": "uberx", 25 | "description": "THE LOW-COST UBER" 26 | }, 27 | { 28 | "upfront_fare_enabled": true, 29 | "capacity": 6, 30 | "product_id": "821415d8-3bd5-4e27-9604-194e4359a449", 31 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberxl2.png", 32 | "cash_enabled": false, 33 | "shared": false, 34 | "short_description": "uberXL", 35 | "display_name": "uberXL", 36 | "product_group": "uberxl", 37 | "description": "LOW-COST RIDES FOR LARGE GROUPS" 38 | }, 39 | { 40 | "upfront_fare_enabled": true, 41 | "capacity": 4, 42 | "product_id": "57c0ff4e-1493-4ef9-a4df-6b961525cf92", 43 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberselect.png", 44 | "cash_enabled": false, 45 | "shared": false, 46 | "short_description": "SELECT", 47 | "display_name": "SELECT", 48 | "product_group": "uberx", 49 | "description": "A STEP ABOVE THE EVERY DAY" 50 | }, 51 | { 52 | "upfront_fare_enabled": true, 53 | "capacity": 4, 54 | "product_id": "d4abaae7-f4d6-4152-91cc-77523e8165a4", 55 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-black.png", 56 | "cash_enabled": false, 57 | "shared": false, 58 | "short_description": "BLACK", 59 | "display_name": "BLACK", 60 | "product_group": "uberblack", 61 | "description": "THE ORIGINAL UBER" 62 | }, 63 | { 64 | "upfront_fare_enabled": true, 65 | "capacity": 6, 66 | "product_id": "8920cb5e-51a4-4fa4-acdf-dd86c5e18ae0", 67 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-suv.png", 68 | "cash_enabled": false, 69 | "shared": false, 70 | "short_description": "SUV", 71 | "display_name": "SUV", 72 | "product_group": "suv", 73 | "description": "ROOM FOR EVERYONE" 74 | }, 75 | { 76 | "upfront_fare_enabled": true, 77 | "capacity": 4, 78 | "product_id": "ff5ed8fe-6585-4803-be13-3ca541235de3", 79 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", 80 | "cash_enabled": false, 81 | "shared": false, 82 | "short_description": "ASSIST", 83 | "display_name": "ASSIST", 84 | "product_group": "uberx", 85 | "description": "uberX with extra assistance" 86 | }, 87 | { 88 | "upfront_fare_enabled": true, 89 | "capacity": 4, 90 | "product_id": "2832a1f5-cfc0-48bb-ab76-7ea7a62060e7", 91 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-wheelchair.png", 92 | "cash_enabled": false, 93 | "shared": false, 94 | "short_description": "WAV", 95 | "display_name": "WAV", 96 | "product_group": "uberx", 97 | "description": "WHEELCHAIR ACCESSIBLE VEHICLES" 98 | }, 99 | { 100 | "upfront_fare_enabled": false, 101 | "capacity": 4, 102 | "product_id": "3ab64887-4842-4c8e-9780-ccecd3a0391d", 103 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-taxi.png", 104 | "cash_enabled": false, 105 | "shared": false, 106 | "short_description": "TAXI", 107 | "display_name": "TAXI", 108 | "product_group": "taxi", 109 | "description": "TAXI WITHOUT THE HASSLE" 110 | } 111 | ] 112 | } -------------------------------------------------------------------------------- /v1/testdata/map-b5512127-a134-4bf4-b1ba-fe9f48f56d9d.json: -------------------------------------------------------------------------------- 1 | { 2 | "request_id":"b5512127-a134-4bf4-b1ba-fe9f48f56d9d", 3 | "href":"https://trip.uber.com/abc123" 4 | } -------------------------------------------------------------------------------- /v1/testdata/place-685-market.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "685 Market St, San Francisco, CA 94103, USA" 3 | } 4 | -------------------------------------------------------------------------------- /v1/testdata/place-wallaby-way.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "P Sherman 42 Wallaby Way Sydney" 3 | } 4 | -------------------------------------------------------------------------------- /v1/testdata/price-estimate-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 15, 3 | "history": [ 4 | { 5 | "status": "completed", 6 | "distance": 1.4780860317, 7 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 8 | "start_time": 1475545183, 9 | "start_city": { 10 | "latitude": 37.7749, 11 | "display_name": "San Francisco", 12 | "longitude": -122.4194 13 | }, 14 | "end_time": 1475545808, 15 | "request_id": "fb0a7c1f-2cf7-4310-bd27-8ba7737362fe", 16 | "request_time": 1475545095 17 | }, 18 | { 19 | "status": "completed", 20 | "distance": 1.2792152568, 21 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 22 | "start_time": 1475513472, 23 | "start_city": { 24 | "latitude": 37.7749, 25 | "display_name": "San Francisco", 26 | "longitude": -122.4194 27 | }, 28 | "end_time": 1475513898, 29 | "request_id": "d72338b0-394d-4f0e-a73c-78d469fa0c6d", 30 | "request_time": 1475513393 31 | }, 32 | { 33 | "status": "completed", 34 | "distance": 1.5084526246, 35 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 36 | "start_time": 1475170251, 37 | "start_city": { 38 | "latitude": 37.7749, 39 | "display_name": "San Francisco", 40 | "longitude": -122.4194 41 | }, 42 | "end_time": 1475171154, 43 | "request_id": "2b61e340-27bd-4937-8304-122009e4a393", 44 | "request_time": 1475170088 45 | }, 46 | { 47 | "status": "completed", 48 | "distance": 1.4705337758, 49 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 50 | "start_time": 1475027766, 51 | "start_city": { 52 | "latitude": 37.7749, 53 | "display_name": "San Francisco", 54 | "longitude": -122.4194 55 | }, 56 | "end_time": 1475028387, 57 | "request_id": "58cb7b3c-fe22-47b4-94c0-2cf08b34f4be", 58 | "request_time": 1475027705 59 | }, 60 | { 61 | "status": "completed", 62 | "distance": 0.6489455763, 63 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 64 | "start_time": 1475002745, 65 | "start_city": { 66 | "latitude": 37.7749, 67 | "display_name": "San Francisco", 68 | "longitude": -122.4194 69 | }, 70 | "end_time": 1475003150, 71 | "request_id": "57be6f97-e10f-411e-a87e-670011c46b55", 72 | "request_time": 1475002656 73 | }, 74 | { 75 | "status": "completed", 76 | "distance": 0.6632030652, 77 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 78 | "start_time": 1475001862, 79 | "start_city": { 80 | "latitude": 37.7749, 81 | "display_name": "San Francisco", 82 | "longitude": -122.4194 83 | }, 84 | "end_time": 1475002218, 85 | "request_id": "0ca65d53-3351-4f3b-b07f-55e4fe4c1ad9", 86 | "request_time": 1475001534 87 | }, 88 | { 89 | "status": "completed", 90 | "distance": 1.3935675129, 91 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 92 | "start_time": 1474995527, 93 | "start_city": { 94 | "latitude": 37.7749, 95 | "display_name": "San Francisco", 96 | "longitude": -122.4194 97 | }, 98 | "end_time": 1474995943, 99 | "request_id": "c0453d97-4330-4ec2-88ab-38678101cc0b", 100 | "request_time": 1474995056 101 | }, 102 | { 103 | "status": "completed", 104 | "distance": 1.5046201975, 105 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 106 | "start_time": 1474909791, 107 | "start_city": { 108 | "latitude": 37.7749, 109 | "display_name": "San Francisco", 110 | "longitude": -122.4194 111 | }, 112 | "end_time": 1474910341, 113 | "request_id": "35822455-e4f5-4339-b763-6fc3ea16dc61", 114 | "request_time": 1474909743 115 | }, 116 | { 117 | "status": "completed", 118 | "distance": 2.4445998557, 119 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 120 | "start_time": 1474685017, 121 | "start_city": { 122 | "latitude": 37.7749, 123 | "display_name": "San Francisco", 124 | "longitude": -122.4194 125 | }, 126 | "end_time": 1474685568, 127 | "request_id": "81a0ffda-a879-4443-beb8-e253f4d19ecc", 128 | "request_time": 1474684872 129 | }, 130 | { 131 | "status": "completed", 132 | "distance": 1.3603866105, 133 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 134 | "start_time": 1474651767, 135 | "start_city": { 136 | "latitude": 37.7749, 137 | "display_name": "San Francisco", 138 | "longitude": -122.4194 139 | }, 140 | "end_time": 1474652253, 141 | "request_id": "97736867-41ca-432a-b7e9-909e66d833ba", 142 | "request_time": 1474651636 143 | } 144 | ], 145 | "limit": 10, 146 | "offset": 0 147 | } -------------------------------------------------------------------------------- /v1/testdata/product-a1111c8c-c720-46c3-8534-2fcdd730040d.json: -------------------------------------------------------------------------------- 1 | { 2 | "upfront_fare_enabled": false, 3 | "capacity": 4, 4 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 5 | "price_details": { 6 | "service_fees": [ 7 | { 8 | "fee": 1.55, 9 | "name": "Booking fee" 10 | } 11 | ], 12 | "cost_per_minute": 0.22, 13 | "distance_unit": "mile", 14 | "minimum": 6.55, 15 | "cost_per_distance": 1.15, 16 | "base": 2, 17 | "cancellation_fee": 5, 18 | "currency_code": "USD" 19 | }, 20 | "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", 21 | "cash_enabled": false, 22 | "shared": false, 23 | "short_description": "uberX", 24 | "display_name": "uberX", 25 | "product_group": "uberx", 26 | "description": "THE LOW-COST UBER" 27 | } -------------------------------------------------------------------------------- /v1/testdata/profile-TEST_TOKEN-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "picture": "https://d1w2poirtb3as9.cloudfront.net/f3be498cb0bbf570aa3d.jpeg", 3 | "first_name": "Uber", 4 | "last_name": "Developer", 5 | "uuid": "f4a416e3-6016-4623-8ec9-d5ee105a6e27", 6 | "rider_id": "8OlTlUG1TyeAQf1JiBZZdkKxuSSOUwu2IkO0Hf9d2HV52Pm25A0NvsbmbnZr85tLVi-s8CckpBK8Eq0Nke4X-no3AcSHfeVh6J5O6LiQt5LsBZDSi4qyVUdSLeYDnTtirw==", 7 | "email": "uberdevelopers@gmail.com", 8 | "mobile_verified": true, 9 | "promo_code": "uberd340ue" 10 | } -------------------------------------------------------------------------------- /v1/testdata/promo-code-pc1.json: -------------------------------------------------------------------------------- 1 | { 2 | "promotion_code": "FREE_RIDEZ", 3 | "description": "$20.00 off your next ride." 4 | } -------------------------------------------------------------------------------- /v1/testdata/receipt-b5512127-a134-4bf4-b1ba-fe9f48f56d9d.json: -------------------------------------------------------------------------------- 1 | { 2 | "request_id": "b5512127-a134-4bf4-b1ba-fe9f48f56d9d", 3 | "subtotal": "$12.78", 4 | "total_charged": "$5.92", 5 | "total_owed": null, 6 | "total_fare": "$5.92", 7 | "currency_code": "USD", 8 | "charge_adjustments": [], 9 | "duration": "00:11:35", 10 | "distance": "1.49", 11 | "distance_label": "miles" 12 | } -------------------------------------------------------------------------------- /v1/testdata/ride-a1111c8c-c720-46c3-8534-2fcdd730040d.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_id": "17cb78a7-b672-4d34-a288-a6c6e44d5315", 3 | "request_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 4 | "status": "accepted", 5 | "surge_multiplier": 1.0, 6 | "shared": true, 7 | "driver": { 8 | "phone_number": "(415)555-1212", 9 | "sms_number": "(415)555-1212", 10 | "rating": 5, 11 | "picture_url": "https://d1w2poirtb3as9.cloudfront.net/img.jpeg", 12 | "name": "Bob" 13 | }, 14 | "vehicle": { 15 | "make": "Bugatti", 16 | "model": "Veyron", 17 | "license_plate": "I<3Uber", 18 | "picture_url": "https://d1w2poirtb3as9.cloudfront.net/car.jpeg" 19 | }, 20 | "location": { 21 | "latitude": 37.3382129093, 22 | "longitude": -121.8863287568, 23 | "bearing": 328 24 | }, 25 | "pickup": { 26 | "latitude": 37.3303463, 27 | "longitude": -121.8890484, 28 | "eta": 5 29 | }, 30 | "destination": { 31 | "latitude": 37.6213129, 32 | "longitude": -122.3789554, 33 | "eta": 19 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /v1/testdata/time-estimate-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "times": [ 3 | { 4 | "localized_display_name": "POOL", 5 | "estimate": 60, 6 | "display_name": "POOL", 7 | "product_id": "26546650-e557-4a7b-86e7-6a3942445247" 8 | }, 9 | { 10 | "localized_display_name": "uberX", 11 | "estimate": 60, 12 | "display_name": "uberX", 13 | "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d" 14 | }, 15 | { 16 | "localized_display_name": "uberXL", 17 | "estimate": 240, 18 | "display_name": "uberXL", 19 | "product_id": "821415d8-3bd5-4e27-9604-194e4359a449" 20 | }, 21 | { 22 | "localized_display_name": "SELECT", 23 | "estimate": 240, 24 | "display_name": "SELECT", 25 | "product_id": "57c0ff4e-1493-4ef9-a4df-6b961525cf92" 26 | }, 27 | { 28 | "localized_display_name": "BLACK", 29 | "estimate": 240, 30 | "display_name": "BLACK", 31 | "product_id": "d4abaae7-f4d6-4152-91cc-77523e8165a4" 32 | }, 33 | { 34 | "localized_display_name": "SUV", 35 | "estimate": 240, 36 | "display_name": "SUV", 37 | "product_id": "8920cb5e-51a4-4fa4-acdf-dd86c5e18ae0" 38 | }, 39 | { 40 | "localized_display_name": "ASSIST", 41 | "estimate": 300, 42 | "display_name": "ASSIST", 43 | "product_id": "ff5ed8fe-6585-4803-be13-3ca541235de3" 44 | }, 45 | { 46 | "localized_display_name": "TAXI", 47 | "estimate": 480, 48 | "display_name": "TAXI", 49 | "product_id": "3ab64887-4842-4c8e-9780-ccecd3a0391d" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /v1/testdata/trip-a1111c8c-c720-46c3-8534-2fcdd730040d.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_id": "17cb78a7-b672-4d34-a288-a6c6e44d5315", 3 | "request_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 4 | "status": "accepted", 5 | "surge_multiplier": 1.0, 6 | "shared": true, 7 | "driver": { 8 | "phone_number": "+14155550000", 9 | "sms_number": "+14155550000", 10 | "rating": 5, 11 | "picture_url": "https:\/\/d1w2poirtb3as9.cloudfront.net\/img.jpeg", 12 | "name": "Bob" 13 | }, 14 | "vehicle": { 15 | "make": "Bugatti", 16 | "model": "Veyron", 17 | "license_plate": "I<3Uber", 18 | "picture_url": "https:\/\/d1w2poirtb3as9.cloudfront.net\/car.jpeg" 19 | }, 20 | "location": { 21 | "latitude": 37.3382129093, 22 | "longitude": -121.8863287568, 23 | "bearing": 328 24 | }, 25 | "pickup": { 26 | "alias": "work", 27 | "latitude": 37.3303463, 28 | "longitude": -121.8890484, 29 | "name": "1455 Market St.", 30 | "address": "1455 Market St, San Francisco, California 94103, US", 31 | "eta": 5 32 | }, 33 | "destination": { 34 | "alias": "home", 35 | "latitude": 37.6213129, 36 | "longitude": -122.3789554, 37 | "name": "685 Market St.", 38 | "address": "685 Market St, San Francisco, CA 94103, USA", 39 | "eta": 19 40 | }, 41 | "waypoints": [ 42 | { 43 | "rider_id":null, 44 | "latitude":37.77508531, 45 | "type":"pickup", 46 | "longitude":-122.3976683872 47 | }, 48 | { 49 | "rider_id":null, 50 | "latitude":37.773133, 51 | "type":"dropoff", 52 | "longitude":-122.415069 53 | }, 54 | { 55 | "rider_id":"8KwsIO_YG6Y2jijSMf", 56 | "latitude":37.7752423, 57 | "type":"dropoff", 58 | "longitude":-122.4175658 59 | } 60 | ], 61 | "riders": [ 62 | { 63 | "rider_id":"8KwsIO_YG6Y2jijSMf", 64 | "first_name":"Alec", 65 | "me": true 66 | }, 67 | { 68 | "rider_id":null, 69 | "first_name":"Kevin", 70 | "me": false 71 | } 72 | ] 73 | } -------------------------------------------------------------------------------- /v1/testdata/trip-current.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_id": "17cb78a7-b672-4d34-a288-a6c6e44d5315", 3 | "request_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", 4 | "status": "accepted", 5 | "surge_multiplier": 1.0, 6 | "shared": true, 7 | "driver": { 8 | "phone_number": "(415)555-1212", 9 | "sms_number": "(415)555-1212", 10 | "rating": 5, 11 | "picture_url": "https:\/\/d1w2poirtb3as9.cloudfront.net\/img.jpeg", 12 | "name": "Bob" 13 | }, 14 | "vehicle": { 15 | "make": "Bugatti", 16 | "model": "Veyron", 17 | "license_plate": "I<3Uber", 18 | "picture_url": "https:\/\/d1w2poirtb3as9.cloudfront.net\/car.jpeg" 19 | }, 20 | "location": { 21 | "latitude": 37.3382129093, 22 | "longitude": -121.8863287568, 23 | "bearing": 328 24 | }, 25 | "pickup": { 26 | "alias": "work", 27 | "latitude": 37.3303463, 28 | "longitude": -121.8890484, 29 | "name": "1455 Market St.", 30 | "address": "1455 Market St, San Francisco, California 94103, US", 31 | "eta": 5 32 | }, 33 | "destination": { 34 | "alias": "home", 35 | "latitude": 37.6213129, 36 | "longitude": -122.3789554, 37 | "name": "685 Market St.", 38 | "address": "685 Market St, San Francisco, CA 94103, USA", 39 | "eta": 19 40 | }, 41 | "waypoints": [ 42 | { 43 | "rider_id":null, 44 | "latitude":37.77508531, 45 | "type":"pickup", 46 | "longitude":-122.3976683872 47 | }, 48 | { 49 | "rider_id":null, 50 | "latitude":37.773133, 51 | "type":"dropoff", 52 | "longitude":-122.415069 53 | }, 54 | { 55 | "rider_id":"8KwsIO_YG6Y2jijSMf", 56 | "latitude":37.7752423, 57 | "type":"dropoff", 58 | "longitude":-122.4175658 59 | } 60 | ], 61 | "riders": [ 62 | { 63 | "rider_id":"8KwsIO_YG6Y2jijSMf", 64 | "first_name":"Alec", 65 | "me": true 66 | }, 67 | { 68 | "rider_id":null, 69 | "first_name":"Kevin", 70 | "me": false 71 | } 72 | ] 73 | } -------------------------------------------------------------------------------- /v1/times.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "net/http" 22 | "time" 23 | 24 | "github.com/orijtech/otils" 25 | ) 26 | 27 | type TimeEstimate struct { 28 | // Expected Time of Arrival for the product (in seconds). 29 | ETASeconds otils.NullableFloat64 `json:"estimate"` 30 | 31 | // Unique identifier representing a specific 32 | // product for a given longitude and latitude. 33 | // For example, uberX in San Francisco will have 34 | // a different ProductID than uberX in Los Angelese. 35 | ProductID string `json:"product_id"` 36 | 37 | // Display name of product. 38 | Name string `json:"display_name"` 39 | 40 | // Localized display name of product. 41 | LocalizedName string `json:"localized_display_name"` 42 | 43 | LimitPerPage int64 `json:"limit"` 44 | } 45 | 46 | var errNilTimeEstimateRequest = errors.New("expecting a non-nil timeEstimateRequest") 47 | 48 | type TimeEstimatesPage struct { 49 | Estimates []*TimeEstimate `json:"times"` 50 | 51 | Count int64 `json:"count,omitempty"` 52 | 53 | Err error 54 | PageNumber uint64 55 | } 56 | 57 | var timeExcludedValues = map[string]bool{ 58 | "seat_count": true, 59 | } 60 | 61 | func (c *Client) EstimateTime(treq *EstimateRequest) (pagesChan chan *TimeEstimatesPage, cancelPaging func(), err error) { 62 | if treq == nil { 63 | return nil, nil, errNilTimeEstimateRequest 64 | } 65 | 66 | pager := new(Pager) 67 | if treq != nil { 68 | *pager = treq.Pager 69 | } 70 | 71 | // Adjust the paging parameters since they'll be heavily used 72 | pager.adjustPageParams() 73 | 74 | requestedMaxPage := pager.MaxPages 75 | pageNumberExceeds := func(pageNumber uint64) bool { 76 | if requestedMaxPage <= 0 { 77 | // No page limit requested at all here 78 | return false 79 | } 80 | 81 | return pageNumber >= uint64(requestedMaxPage) 82 | } 83 | 84 | cancelChan, cancelFn := makeCancelParadigm() 85 | estimatesPageChan := make(chan *TimeEstimatesPage) 86 | go func() { 87 | defer close(estimatesPageChan) 88 | 89 | throttleDuration := 150 * time.Millisecond 90 | pageNumber := uint64(0) 91 | 92 | canPage := true 93 | for canPage { 94 | tp := new(TimeEstimatesPage) 95 | tp.PageNumber = pageNumber 96 | 97 | qv, err := otils.ToURLValues(treq) 98 | if err != nil { 99 | tp.Err = err 100 | estimatesPageChan <- tp 101 | return 102 | } 103 | 104 | fullURL := fmt.Sprintf("%s/estimates/time?%s", c.baseURL(), qv.Encode()) 105 | req, err := http.NewRequest("GET", fullURL, nil) 106 | if err != nil { 107 | tp.Err = err 108 | estimatesPageChan <- tp 109 | return 110 | } 111 | 112 | slurp, _, err := c.doReq(req) 113 | if err != nil { 114 | tp.Err = err 115 | estimatesPageChan <- tp 116 | return 117 | } 118 | 119 | if err := json.Unmarshal(slurp, tp); err != nil { 120 | tp.Err = err 121 | estimatesPageChan <- tp 122 | return 123 | } 124 | 125 | estimatesPageChan <- tp 126 | 127 | if tp.Count <= 0 { 128 | // No more items to page 129 | return 130 | } 131 | 132 | select { 133 | case <-cancelChan: 134 | // The user has canceled the paging 135 | canPage = false 136 | return 137 | 138 | case <-time.After(throttleDuration): 139 | // Do nothing here, the throttle time expired. 140 | } 141 | 142 | // Now it is time to adjust the next offset accordingly with the remaining count 143 | 144 | // Increment the page number as well 145 | pageNumber += 1 146 | if pageNumberExceeds(pageNumber) { 147 | return 148 | } 149 | 150 | pager.StartOffset += pager.LimitPerPage 151 | } 152 | }() 153 | 154 | return estimatesPageChan, cancelFn, nil 155 | } 156 | -------------------------------------------------------------------------------- /v1/uber.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 orijtech. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package uber 16 | 17 | import ( 18 | "encoding/json" 19 | "strings" 20 | "sync" 21 | ) 22 | 23 | func makeCancelParadigm() (<-chan bool, func()) { 24 | var cancelOnce sync.Once 25 | cancelChan := make(chan bool, 1) 26 | cancelFn := func() { 27 | cancelOnce.Do(func() { 28 | close(cancelChan) 29 | }) 30 | } 31 | 32 | return cancelChan, cancelFn 33 | } 34 | 35 | type Error struct { 36 | Meta interface{} `json:"meta"` 37 | Errors []*statusCodedError `json:"errors"` 38 | 39 | memoized string 40 | } 41 | 42 | func (ue *Error) Error() string { 43 | if ue == nil { 44 | return "" 45 | } 46 | if ue.memoized != "" { 47 | return ue.memoized 48 | } 49 | 50 | // Otherwise create it 51 | var errsList []string 52 | for _, sce := range ue.Errors { 53 | errsList = append(errsList, sce.Error()) 54 | } 55 | ue.memoized = strings.Join(errsList, "\n") 56 | return ue.memoized 57 | } 58 | 59 | var _ error = (*Error)(nil) 60 | var _ error = (*statusCodedError)(nil) 61 | 62 | type statusCodedError struct { 63 | // The json tags are intentionally reversed 64 | // because an uber status coded error looks 65 | // like this: 66 | // { 67 | // "status":404, 68 | // "code":"unknown_place_id", 69 | // "title":"Could not resolve the given place_id." 70 | // } 71 | // of which the above definitions seem reversed compared to 72 | // Go's net/http Request where Status is a message and StatusCode is an int. 73 | Code int `json:"status"` 74 | Message string `json:"code"` 75 | Title string `json:"title"` 76 | 77 | memoizedErr string 78 | } 79 | 80 | func (sce *statusCodedError) Error() string { 81 | if sce == nil { 82 | return "" 83 | } 84 | if sce.memoizedErr == "" { 85 | blob, _ := json.Marshal(sce) 86 | sce.memoizedErr = string(blob) 87 | } 88 | return sce.memoizedErr 89 | } 90 | --------------------------------------------------------------------------------