├── .travis.yml ├── LICENSE ├── README.md ├── digits ├── accounts.go ├── accounts_test.go ├── contacts.go ├── contacts_test.go ├── digits.go ├── digits_test.go ├── doc.go ├── errors.go └── errors_test.go ├── examples ├── README.md └── digits.go └── test /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | - 1.8 5 | - tip 6 | install: 7 | - go get github.com/golang/lint/golint 8 | - go get -v -t ./digits 9 | script: 10 | - ./test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dalton Hubble 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # go-digits [![Build Status](https://travis-ci.org/dghubble/go-digits.png)](https://travis-ci.org/dghubble/go-digits) [![GoDoc](https://godoc.org/github.com/dghubble/go-digits?status.png)](https://godoc.org/github.com/dghubble/go-digits) 3 | 4 | 5 | go-digits is a Go client library for the [Digits](https://get.digits.com/) API. Check the [usage](#usage) section or the [examples](examples) to learn how to access the Digits API. 6 | 7 | ### Features 8 | 9 | * AccountService for getting Digits accounts 10 | * Get verified user phone numbers and email addresses 11 | * ContactService for finding matching contacts ("friends") 12 | * Digits API Client accepts any OAuth1 `http.Client` 13 | 14 | ## Install 15 | 16 | go get github.com/dghubble/go-digits/digits 17 | 18 | ## Docs 19 | 20 | Read [GoDoc](https://godoc.org/github.com/dghubble/go-digits/digits) 21 | 22 | ## Usage 23 | 24 | The `digits` package provides a `Client` for accessing the Digits API. Here is an example request for a Digit user's Account. 25 | 26 | ```go 27 | import ( 28 | "github.com/dghubble/go-digits/digits" 29 | "github.com/dghubble/oauth1" 30 | ) 31 | 32 | config := oauth1.NewConfig("consumerKey", "consumerSecret") 33 | token := oauth1.NewToken("accessToken", "accessSecret") 34 | // OAuth1 http.Client will automatically authorize Requests 35 | httpClient := config.Client(oauth1.NoContext, token) 36 | 37 | // Digits client 38 | client := digits.NewClient(httpClient) 39 | // get current user's Digits Account 40 | account, resp, err := client.Accounts.Account() 41 | ``` 42 | 43 | ### Authentication 44 | 45 | The API client accepts any `http.Client` capable of signing OAuth1 requests to handle authorization. See the OAuth1 package [dghubble/oauth1](https://github.com/dghubble/oauth1) for details and examples. 46 | 47 | To implement Login with Digits for web or mobile, see the gologin [package](https://github.com/dghubble/gologin) and [examples](https://github.com/dghubble/gologin/tree/master/examples/digits). 48 | 49 | ## Contributing 50 | 51 | See the [Contributing Guide](https://gist.github.com/dghubble/be682c123727f70bcfe7). 52 | 53 | ## License 54 | 55 | [MIT License](LICENSE) 56 | 57 | 58 | -------------------------------------------------------------------------------- /digits/accounts.go: -------------------------------------------------------------------------------- 1 | package digits 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/dghubble/sling" 7 | ) 8 | 9 | // Account is a Digits user account. 10 | type Account struct { 11 | AccessToken AccessToken `json:"access_token"` 12 | CreatedAt string `json:"created_at"` 13 | Email Email `json:"email_address"` 14 | ID int64 `json:"id"` 15 | IDStr string `json:"id_str"` 16 | PhoneNumber string `json:"phone_number"` 17 | VerificationType string `json:"verification_type"` 18 | } 19 | 20 | // Email is a Digits user email. 21 | type Email struct { 22 | Address string `json:"address"` 23 | Verified bool `json:"is_verified"` 24 | } 25 | 26 | // AccessToken is a Digits OAuth1 access token and secret. 27 | type AccessToken struct { 28 | Token string `json:"token"` 29 | Secret string `json:"secret"` 30 | } 31 | 32 | // AccountService provides methods for accessing Digits Accounts. 33 | type AccountService struct { 34 | sling *sling.Sling 35 | } 36 | 37 | // NewAccountService returns a new AccountService. 38 | func NewAccountService(sling *sling.Sling) *AccountService { 39 | return &AccountService{ 40 | sling: sling.Path("sdk/"), 41 | } 42 | } 43 | 44 | // Account returns the authenticated user Account. 45 | func (s *AccountService) Account() (*Account, *http.Response, error) { 46 | account := new(Account) 47 | apiError := new(APIError) 48 | resp, err := s.sling.New().Get("account.json").Receive(account, apiError) 49 | return account, resp, firstError(err, apiError) 50 | } 51 | -------------------------------------------------------------------------------- /digits/accounts_test.go: -------------------------------------------------------------------------------- 1 | package digits 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestAccountService_Account(t *testing.T) { 12 | httpClient, mux, server := testServer() 13 | defer server.Close() 14 | 15 | mux.HandleFunc("/1.1/sdk/account.json", func(w http.ResponseWriter, r *http.Request) { 16 | assertMethod(t, "GET", r) 17 | w.Header().Set("Content-Type", "application/json") 18 | fmt.Fprintf(w, `{"id": 11, "id_str": "11", "phone_number": "0123456789", "email_address":{"address":"user@example.com","is_verified":true}, "access_token": {"token": "t", "secret": "s"}, "verification_type":"sms"}`) 19 | }) 20 | expected := &Account{ 21 | AccessToken: AccessToken{Token: "t", Secret: "s"}, 22 | Email: Email{Address: "user@example.com", Verified: true}, 23 | ID: 11, 24 | IDStr: "11", 25 | PhoneNumber: "0123456789", 26 | VerificationType: "sms", 27 | } 28 | 29 | client := NewClient(httpClient) 30 | account, _, err := client.Accounts.Account() 31 | assert.Nil(t, err) 32 | assert.Equal(t, expected, account) 33 | } 34 | 35 | func TestAccountService_APIError(t *testing.T) { 36 | httpClient, mux, server := testServer() 37 | defer server.Close() 38 | 39 | mux.HandleFunc("/1.1/sdk/account.json", func(w http.ResponseWriter, r *http.Request) { 40 | assertMethod(t, "GET", r) 41 | w.Header().Set("Content-Type", "application/json") 42 | w.WriteHeader(400) 43 | fmt.Fprintf(w, `{"errors": [{"message": "Bad Authentication data.", "code": 215}]}`) 44 | }) 45 | expected := &APIError{ 46 | Errors: []ErrorDetail{ 47 | ErrorDetail{Message: "Bad Authentication data.", Code: 215}, 48 | }, 49 | } 50 | 51 | client := NewClient(httpClient) 52 | _, _, err := client.Accounts.Account() 53 | if assert.Error(t, err) { 54 | assert.Equal(t, expected, err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /digits/contacts.go: -------------------------------------------------------------------------------- 1 | package digits 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/dghubble/sling" 7 | ) 8 | 9 | // Contacts represents a cursored subset of a set of contacts. 10 | type Contacts struct { 11 | NextCursor string `json:"next_cursor"` 12 | // matched contacts are partially hydrated Accounts (i.e. no token, phone) 13 | Users []Account `json:"users"` 14 | } 15 | 16 | // ContactService provides methods for accessing Digits contacts. 17 | type ContactService struct { 18 | sling *sling.Sling 19 | } 20 | 21 | // NewContactService returns a new ContactService. 22 | func NewContactService(sling *sling.Sling) *ContactService { 23 | return &ContactService{ 24 | sling: sling.Path("contacts/"), 25 | } 26 | } 27 | 28 | // MatchesParams are the parameters for ContactService.Matches 29 | type MatchesParams struct { 30 | NextCursor string `url:"next_cursor,omitempty"` 31 | Count int `url:"count,omitempty"` 32 | } 33 | 34 | // Matches returns Contacts with the cursored Digits Accounts which have logged 35 | // into the Digits application and are known to the authenticated user Account 36 | // via address book uploads. 37 | func (s *ContactService) Matches(params *MatchesParams) (*Contacts, *http.Response, error) { 38 | contacts := new(Contacts) 39 | apiError := new(APIError) 40 | resp, err := s.sling.New().Get("users_and_uploaded_by.json").QueryStruct(params).Receive(contacts, apiError) 41 | return contacts, resp, firstError(err, apiError) 42 | } 43 | -------------------------------------------------------------------------------- /digits/contacts_test.go: -------------------------------------------------------------------------------- 1 | package digits 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestContactService_Matches(t *testing.T) { 12 | httpClient, mux, server := testServer() 13 | defer server.Close() 14 | 15 | mux.HandleFunc("/1.1/contacts/users_and_uploaded_by.json", func(w http.ResponseWriter, r *http.Request) { 16 | assertMethod(t, "GET", r) 17 | assertQuery(t, map[string]string{"count": "20", "next_cursor": "9876543"}, r) 18 | w.Header().Set("Content-Type", "application/json") 19 | fmt.Fprintf(w, `{"users": [{"id": 11, "id_str": "11"}], "next_cursor": "9876543"}`) 20 | }) 21 | expected := &Contacts{ 22 | Users: []Account{ 23 | {ID: 11, IDStr: "11"}, 24 | }, 25 | NextCursor: "9876543", 26 | } 27 | 28 | client := NewClient(httpClient) 29 | params := &MatchesParams{Count: 20, NextCursor: "9876543"} 30 | contacts, _, err := client.Contacts.Matches(params) 31 | assert.Nil(t, err) 32 | assert.Equal(t, expected, contacts) 33 | } 34 | 35 | func TestContactService_MatchesAPIError(t *testing.T) { 36 | httpClient, mux, server := testServer() 37 | defer server.Close() 38 | 39 | mux.HandleFunc("/1.1/contacts/users_and_uploaded_by.json", func(w http.ResponseWriter, r *http.Request) { 40 | w.Header().Set("Content-Type", "application/json") 41 | w.WriteHeader(400) 42 | fmt.Fprintf(w, `{"errors": [{"message": "Bad Authentication data.", "code": 215}]}`) 43 | }) 44 | expected := &APIError{ 45 | Errors: []ErrorDetail{ 46 | ErrorDetail{Message: "Bad Authentication data.", Code: 215}, 47 | }, 48 | } 49 | 50 | client := NewClient(httpClient) 51 | _, _, err := client.Contacts.Matches(&MatchesParams{}) 52 | if assert.Error(t, err) { 53 | assert.Equal(t, expected, err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /digits/digits.go: -------------------------------------------------------------------------------- 1 | package digits 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/dghubble/sling" 7 | ) 8 | 9 | // DigitsAPI and version 10 | const ( 11 | DigitsAPI = "https://api.digits.com" 12 | apiVersion = "/1.1/" 13 | ) 14 | 15 | // Client is a Digits client for making Digits API requests. 16 | type Client struct { 17 | sling *sling.Sling 18 | // Digits API Services 19 | Accounts *AccountService 20 | Contacts *ContactService 21 | } 22 | 23 | // NewClient returns a new Client. 24 | func NewClient(httpClient *http.Client) *Client { 25 | base := sling.New().Client(httpClient).Base(DigitsAPI + apiVersion) 26 | return &Client{ 27 | sling: base, 28 | Accounts: NewAccountService(base.New()), 29 | Contacts: NewContactService(base.New()), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /digits/digits_test.go: -------------------------------------------------------------------------------- 1 | package digits 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // testServer returns an http Client, ServeMux, and Server. The client proxies 13 | // requests to the server and handlers can be registered on the mux to handle 14 | // requests. The caller must close the test server. 15 | func testServer() (*http.Client, *http.ServeMux, *httptest.Server) { 16 | mux := http.NewServeMux() 17 | server := httptest.NewServer(mux) 18 | transport := &RewriteTransport{&http.Transport{ 19 | Proxy: func(req *http.Request) (*url.URL, error) { 20 | return url.Parse(server.URL) 21 | }, 22 | }} 23 | client := &http.Client{Transport: transport} 24 | return client, mux, server 25 | } 26 | 27 | // RewriteTransport rewrites https requests to http to avoid TLS cert issues 28 | // during testing. 29 | type RewriteTransport struct { 30 | Transport http.RoundTripper 31 | } 32 | 33 | // RoundTrip rewrites the request scheme to http and calls through to the 34 | // composed RoundTripper or if it is nil, to the http.DefaultTransport. 35 | func (t *RewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) { 36 | req.URL.Scheme = "http" 37 | if t.Transport == nil { 38 | return http.DefaultTransport.RoundTrip(req) 39 | } 40 | return t.Transport.RoundTrip(req) 41 | } 42 | 43 | // assertMethod tests that the Request has the expected HTTP method. 44 | func assertMethod(t *testing.T, expectedMethod string, req *http.Request) { 45 | assert.Equal(t, expectedMethod, req.Method) 46 | } 47 | 48 | // assertQuery tests that the Request has the expected url query key/val pairs 49 | func assertQuery(t *testing.T, expected map[string]string, req *http.Request) { 50 | queryValues := req.URL.Query() 51 | expectedValues := url.Values{} 52 | for key, value := range expected { 53 | expectedValues.Add(key, value) 54 | } 55 | assert.Equal(t, expectedValues, queryValues) 56 | } 57 | -------------------------------------------------------------------------------- /digits/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package digits provides a Client for the Digits API. 3 | 4 | The digits package provides a Client for accessing Digits API services. Here 5 | is an example request for a Digit user's Account. 6 | 7 | import ( 8 | "github.com/dghubble/go-digits/digits" 9 | "github.com/dghubble/oauth1" 10 | ) 11 | 12 | config := oauth1.NewConfig("consumerKey", "consumerSecret") 13 | token := oauth1.NewToken("accessToken", "accessTokenSecret") 14 | // OAuth1 http.Client will automatically authorize Requests 15 | httpClient := config.Client(token) 16 | 17 | // Digits client 18 | client := digits.NewClient(httpClient) 19 | // get current user's Digits Account 20 | account, resp, err := client.Accounts.Account() 21 | 22 | The API client accepts any http.Client capable of signing OAuth1 requests to 23 | handle authorization. 24 | 25 | See the OAuth1 package https://github.com/dghubble/oauth1 for authorization 26 | details and examples. 27 | 28 | To implement Login with Digits, see https://github.com/dghubble/gologin. 29 | */ 30 | package digits 31 | -------------------------------------------------------------------------------- /digits/errors.go: -------------------------------------------------------------------------------- 1 | package digits 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // APIError represents a Digits API Error response 8 | type APIError struct { 9 | Errors []ErrorDetail `json:"errors"` 10 | } 11 | 12 | // ErrorDetail represents an individual error in an APIError. 13 | type ErrorDetail struct { 14 | Message string `json:"message"` 15 | Code int `json:"code"` 16 | } 17 | 18 | func (e *APIError) Error() string { 19 | if len(e.Errors) > 0 { 20 | err := e.Errors[0] 21 | return fmt.Sprintf("digits: %d %v", err.Code, err.Message) 22 | } 23 | return "" 24 | } 25 | 26 | // Empty returns true if the Errors slice is empty, false otherwise. 27 | func (e APIError) Empty() bool { 28 | if len(e.Errors) == 0 { 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | // firstError returns the first error among err and apiError which is non-nil 35 | // (or non-Empty in the case of apiError) or nil if neither represent errors. 36 | // 37 | // A common use case is an API which prefers to return any a network error, 38 | // if any, and return an API error in the absence of network errors. 39 | func firstError(err error, apiError *APIError) error { 40 | if err != nil { 41 | return err 42 | } 43 | if apiError != nil && !apiError.Empty() { 44 | return apiError 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /digits/errors_test.go: -------------------------------------------------------------------------------- 1 | package digits 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var testAPIError = &APIError{ 11 | Errors: []ErrorDetail{ 12 | ErrorDetail{Message: "Could not authenticate you.", Code: 32}, 13 | }, 14 | } 15 | var errTestError = fmt.Errorf("unknown host") 16 | 17 | func TestAPIError_ErrorString(t *testing.T) { 18 | err := &APIError{} 19 | assert.Equal(t, "", err.Error()) 20 | assert.Equal(t, "digits: 32 Could not authenticate you.", testAPIError.Error()) 21 | } 22 | 23 | func TestAPIError_Empty(t *testing.T) { 24 | err := APIError{} 25 | assert.True(t, err.Empty()) 26 | assert.False(t, testAPIError.Empty()) 27 | } 28 | 29 | func TestFirstError(t *testing.T) { 30 | cases := []struct { 31 | httpError error 32 | apiError *APIError 33 | expected error 34 | }{ 35 | {nil, nil, nil}, 36 | {nil, &APIError{}, nil}, 37 | {nil, testAPIError, testAPIError}, 38 | {errTestError, &APIError{}, errTestError}, 39 | {errTestError, testAPIError, errTestError}, 40 | } 41 | for _, c := range cases { 42 | err := firstError(c.httpError, c.apiError) 43 | assert.Equal(t, c.expected, err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Examples 3 | 4 | ## Digits API 5 | 6 | A user grants a Digits consumer application access to his/her Digits resources (phone number, contacts). As a result, the application receives an OAuth1 access token and secret. 7 | 8 | To make requests as an application, on behalf of the user, a client requires the application consumer key and secret and the user access token and secret. 9 | 10 | export DIGITS_CONSUMER_KEY=xxx 11 | export DIGITS_CONSUMER_SECRET=xxx 12 | export DIGITS_ACCESS_TOKEN=xxx 13 | export DIGITS_ACCESS_SECRET=xxx 14 | 15 | Get the dependencies for the examples 16 | 17 | cd examples 18 | go get . 19 | 20 | ## Accounts API 21 | 22 | Get the current user's Digits `Account` and lookup matching contacts. 23 | 24 | go run digits.go 25 | -------------------------------------------------------------------------------- /examples/digits.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/coreos/pkg/flagutil" 10 | "github.com/dghubble/go-digits/digits" 11 | "github.com/dghubble/oauth1" 12 | ) 13 | 14 | func main() { 15 | flags := flag.NewFlagSet("digits-example", flag.ExitOnError) 16 | consumerKey := flags.String("consumer-key", "", "Digits Consumer Key") 17 | consumerSecret := flags.String("consumer-secret", "", "Digits Consumer Secret") 18 | accessToken := flags.String("access-token", "", "Digits Access Token") 19 | accessSecret := flags.String("access-secret", "", "Digits Access Secret") 20 | flags.Parse(os.Args[1:]) 21 | flagutil.SetFlagsFromEnv(flags, "DIGITS") 22 | 23 | if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" { 24 | log.Fatal("Consumer key/secret and Access token/secret required") 25 | } 26 | 27 | config := oauth1.NewConfig(*consumerKey, *consumerSecret) 28 | token := oauth1.NewToken(*accessToken, *accessSecret) 29 | // OAuth1 http.Client will automatically authorize Requests 30 | httpClient := config.Client(oauth1.NoContext, token) 31 | 32 | // Digits client 33 | client := digits.NewClient(httpClient) 34 | 35 | // get current user's Digits Account 36 | account, _, err := client.Accounts.Account() 37 | if err != nil { 38 | fmt.Println(err) 39 | } else { 40 | fmt.Printf("ACCOUNT:\n%+v\n", account) 41 | } 42 | 43 | // get Digits users who have signed up for the Digits App and are known to 44 | // the current user 45 | matchParams := &digits.MatchesParams{Count: 20} 46 | contacts, _, _ := client.Contacts.Matches(matchParams) 47 | fmt.Printf("CONTACT MATCHES:\n%+v\n", contacts) 48 | } 49 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | PKGS=$(go list ./... | grep -v /examples) 5 | FORMATTABLE="$(find . -maxdepth 1 -type d)" 6 | LINTABLE=$(go list ./...) 7 | 8 | go test $PKGS -cover 9 | go vet $PKGS 10 | 11 | echo "Checking gofmt..." 12 | fmtRes=$(gofmt -l $FORMATTABLE) 13 | if [ -n "${fmtRes}" ]; then 14 | echo -e "gofmt checking failed:\n${fmtRes}" 15 | exit 2 16 | fi 17 | 18 | echo "Checking golint..." 19 | lintRes=$(echo $LINTABLE | xargs -n 1 golint) 20 | if [ -n "${lintRes}" ]; then 21 | echo -e "golint checking failed:\n${lintRes}" 22 | exit 2 23 | fi --------------------------------------------------------------------------------