├── README.md ├── .gitignore ├── lookup_test.go ├── message_test.go ├── call_test.go ├── util.go ├── error.go ├── doc.go ├── LICENSE ├── client.go ├── lookup.go ├── list_test.go ├── README ├── message.go ├── list.go └── call.go /README.md: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /lookup_test.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestLookup(t *testing.T) { 9 | lookup, err := TestClient.Lookup(ToPhoneNumber) 10 | if err != nil { 11 | t.Fatalf("Failed: %s", err.Error()) 12 | } 13 | bs, err := json.MarshalIndent(lookup, "", " ") 14 | if err != nil { 15 | t.Fatalf("Failed: %s", err.Error()) 16 | } 17 | t.Logf("Lookup Result:\n%s\n", string(bs)) 18 | } 19 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | // This test sends a test SMS to ToPhoneNumber 9 | func TestSendSMS(t *testing.T) { 10 | msg, err := TestClient.SendSMS(FromPhoneNumber, ToPhoneNumber, "Hello, world!") 11 | if err != nil { 12 | t.Fatalf("Failed: %s", err.Error()) 13 | } 14 | bs, err := json.MarshalIndent(msg, "", " ") 15 | if err != nil { 16 | t.Fatalf("Failed: %s", err.Error()) 17 | } 18 | t.Logf("Message Sent:\n%s\n", string(bs)) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /call_test.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | // This test calls ToPhoneNumber and also forwards the call to ToPhoneNumber. 10 | // ToPhoneNumber should expect two calls. 11 | func TestCall(t *testing.T) { 12 | callbackPostURL := fmt.Sprintf("http://twimlets.com/forward?PhoneNumber=%s", ToPhoneNumber) 13 | call, err := TestClient.Call(FromPhoneNumber, ToPhoneNumber, callbackPostURL) 14 | if err != nil { 15 | t.Fatalf("Failed: %s", err.Error()) 16 | } 17 | bs, err := json.MarshalIndent(call, "", " ") 18 | if err != nil { 19 | t.Fatalf("Failed: %s", err.Error()) 20 | } 21 | t.Logf("Call:\n%s\n", string(bs)) 22 | } 23 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // YMD is used to format time.Time for querying the Twilio REST API 8 | const YMD = "2006-01-02" 9 | 10 | // Time is a wrapper around time.Time to support JSON marshalling to/from 11 | // the Twilio REST API, which uses RFC1123Z. 12 | type Time struct { 13 | time.Time 14 | } 15 | 16 | // jsonRFC1123Z is the time formatting string for utwil.Time 17 | const jsonRFC1123Z = `"` + time.RFC1123Z + `"` 18 | 19 | // MarshalJSON marshals time.Time into the time.RFC1123Z format 20 | func (t *Time) MarshalJSON() ([]byte, error) { 21 | str := t.Format(jsonRFC1123Z) 22 | return []byte(str), nil 23 | } 24 | 25 | // UnmarshalJSON unmarshals time.Time from the time.RFC1123Z format 26 | func (t *Time) UnmarshalJSON(data []byte) error { 27 | ot, err := time.Parse(jsonRFC1123Z, string(data)) 28 | if err != nil { 29 | return err 30 | } 31 | t.Time = ot 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // RESTException represents an error returned by the Twilio API 9 | // 10 | // Details: 11 | // 12 | // https://www.twilio.com/docs/errors 13 | // 14 | type RESTException struct { 15 | Code *int `json:"code"` 16 | Message string `json:"message"` 17 | MoreInfo string `json:"more_info"` 18 | Status interface{} `json:"status"` 19 | } 20 | 21 | // Check the returned JSON for a utwil.RESTException, and return that as an 22 | // error if so. 23 | func checkJSON(buf []byte) error { 24 | re := &RESTException{} 25 | err := json.Unmarshal(buf, re) 26 | if err != nil { 27 | return err 28 | } 29 | if re.Code != nil && *re.Code != 0 { 30 | return re 31 | } 32 | return nil 33 | } 34 | 35 | // Print the RESTException in a human-readable form. 36 | func (r RESTException) Error() string { 37 | if r.Code != nil { 38 | return fmt.Sprintf("Code %d: %s", *r.Code, r.Message) 39 | } else if r.Status != nil { 40 | return fmt.Sprintf("Status %d: %s", r.Status, r.Message) 41 | } 42 | return r.Message 43 | } 44 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package utwil contains Go utilities for dealing with the Twilio API 2 | // 3 | // The most-used data structure is the Client, which stores credentials and 4 | // has useful methods for interacing with Twilio. The current supported feature 5 | // set includes the sending of Calls and Messages, retrieval of Calls and 6 | // Messages, and the lookup of phone numbers. 7 | // 8 | // These actions will incur the appropriate costs on your Twilio account. 9 | // 10 | // Before go test, populate env vars TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, 11 | // TWILIO_DEFAULT_TO, and TWILIO_DEFAULT_FROM. 12 | // 13 | // Start with: 14 | // 15 | // client := utwil.NewClient(AccoutSID, AuthToken) 16 | // 17 | // Commonly used actions have convenience functions: 18 | // 19 | // msg, err := client.SendSMS("+15551231234", "+15559879876", "Hello, world!") 20 | // 21 | // For more complicated requests, populate the respective XxxxxReq struct 22 | // and call the SubmitXxxxx() method: 23 | // 24 | // msgReq := utwil.MessageReq{ 25 | // From: "+15559871234", 26 | // To: "+15551231234", 27 | // Body: "Hello, world!", 28 | // StatusCallback: "https://post.here.com/when/msg/status/changes.twiml", 29 | // } 30 | // msg, err := client.SubmitMessage(msgReq) 31 | // 32 | package utwil 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | utwil - Go utilities for Twilio 2 | 3 | Copyright (c) 2015-2016 - wyc 4 | 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | // At the time of writing, the current API version was released on Apr. 1, 2010 12 | const ( 13 | BaseURL = "https://api.twilio.com" 14 | LookupURL = "https://lookups.twilio.com/v1" 15 | 16 | APIVersion = "2010-04-01" 17 | ) 18 | 19 | // Client stores Twilio API credentials 20 | type Client struct { 21 | AccountSID string 22 | AuthToken string 23 | HTTPClient *http.Client 24 | } 25 | 26 | // NewClient exists as a stable interface to create a new utwil.Client. 27 | func NewClient(accountSID, authToken string) Client { 28 | return Client{accountSID, authToken, &http.Client{}} 29 | } 30 | 31 | func (c *Client) getJSON(url string, result interface{}) error { 32 | req, err := http.NewRequest("GET", url, nil) 33 | if err != nil { 34 | return fmt.Errorf("GetJSON(): %s", err) 35 | } 36 | req.SetBasicAuth(c.AccountSID, c.AuthToken) 37 | resp, err := c.HTTPClient.Do(req) 38 | if err != nil { 39 | return fmt.Errorf("GetJSON(): %s", err) 40 | } 41 | 42 | if resp.StatusCode != 200 { 43 | re := RESTException{} 44 | json.NewDecoder(resp.Body).Decode(&re) 45 | return re 46 | } 47 | return json.NewDecoder(resp.Body).Decode(&result) 48 | } 49 | 50 | func (c *Client) postForm(url string, values url.Values, result interface{}) error { 51 | req, err := http.NewRequest("POST", url, strings.NewReader(values.Encode())) 52 | if err != nil { 53 | return fmt.Errorf("PostForm(): %s", err) 54 | } 55 | req.SetBasicAuth(c.AccountSID, c.AuthToken) 56 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 57 | resp, err := c.HTTPClient.Do(req) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // HTTP 2xx codes are successful, others are errors 63 | if resp.StatusCode >= 300 || resp.StatusCode < 200 { 64 | err := RESTException{} 65 | json.NewDecoder(resp.Body).Decode(&err) 66 | return err 67 | } 68 | return json.NewDecoder(resp.Body).Decode(&result) 69 | } 70 | 71 | func (c *Client) urlPrefix() string { 72 | return fmt.Sprintf("%s/%s/Accounts/%s", BaseURL, APIVersion, c.AccountSID) 73 | } 74 | 75 | func (c *Client) callsURL() string { 76 | return fmt.Sprintf("%s/Calls.json", c.urlPrefix()) 77 | } 78 | 79 | func (c *Client) messagesURL() string { 80 | return fmt.Sprintf("%s/Messages.json", c.urlPrefix()) 81 | } 82 | -------------------------------------------------------------------------------- /lookup.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // LookupReq is the Go-representation of Twilio REST API's lookup request. 9 | // 10 | // Details: 11 | // https://www.twilio.com/docs/api/rest/lookups#lookups-query-parameters 12 | // 13 | type LookupReq struct { 14 | PhoneNumber string 15 | Type string 16 | CountryCode string 17 | } 18 | 19 | // SubmitLookup sends a lookup request populating form fields only if they 20 | // contain a non-zero value. 21 | func (c *Client) SubmitLookup(req LookupReq) (Lookup, error) { 22 | // @TODO wait until github.com/gorilla/schema supports struct-to-url.Values 23 | values := url.Values{} 24 | if req.Type != "" { 25 | values.Add("Type", req.Type) 26 | } 27 | if req.CountryCode != "" { 28 | values.Add("CountryCode", req.CountryCode) 29 | } 30 | url := fmt.Sprintf("%s/PhoneNumbers/%s?%s", LookupURL, req.PhoneNumber, values.Encode()) 31 | res := Lookup{} 32 | err := c.getJSON(url, &res) 33 | return res, err 34 | } 35 | 36 | // Lookup is the Go-representation of Twilio REST API's lookup. 37 | // 38 | // Details: 39 | // 40 | // https://www.twilio.com/docs/api/rest/lookups 41 | // 42 | type Lookup struct { 43 | Carrier *struct { 44 | ErrorCode *int `json:"error_code"` 45 | MobileCountryCode string `json:"mobile_country_code"` 46 | MobileNetworkCode string `json:"mobile_network_code"` 47 | Name string `json:"name"` 48 | Type string `json:"type"` 49 | } `json:"carrier"` 50 | CountryCode string `json:"country_code"` 51 | NationalFormat string `json:"national_format"` 52 | PhoneNumber string `json:"phone_number"` 53 | URL string `json:"url"` 54 | } 55 | 56 | // Lookup looks up a phone number's details including the carrier (Type=carrier) 57 | // 58 | // Example: 59 | // 60 | // lookup, err := client.Lookup("+15551231234") 61 | // // handle err 62 | // fmt.Println(lookup.Carrier.Type) // "mobile", "landline", or "voip" 63 | // 64 | func (c *Client) Lookup(phoneNumber string) (Lookup, error) { 65 | req := LookupReq{ 66 | PhoneNumber: phoneNumber, 67 | Type: "carrier", 68 | } 69 | return c.SubmitLookup(req) 70 | } 71 | 72 | // LookupNoCarrier looks up a phone number's details without the carrier 73 | func (c *Client) LookupNoCarrier(phoneNumber string) (Lookup, error) { 74 | req := LookupReq{PhoneNumber: phoneNumber} 75 | return c.SubmitLookup(req) 76 | } 77 | -------------------------------------------------------------------------------- /list_test.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var ( 11 | AccountSID = os.Getenv("TWILIO_ACCOUNT_SID") 12 | AuthToken = os.Getenv("TWILIO_AUTH_TOKEN") 13 | ToPhoneNumber = os.Getenv("TWILIO_DEFAULT_TO") 14 | FromPhoneNumber = os.Getenv("TWILIO_DEFAULT_FROM") 15 | TestClient = NewClient(AccountSID, AuthToken) 16 | ) 17 | 18 | func init() { 19 | if AccountSID == "" { 20 | log.Fatalf("Testing env var TWILIO_ACCOUNT_SID is unset") 21 | } else if AuthToken == "" { 22 | log.Fatalf("Testing env var TWILIO_AUTH_TOKEN is unset") 23 | } else if ToPhoneNumber == "" { 24 | log.Fatalf("Testing env var TWILIO_DEFAULT_TO is unset") 25 | } else if FromPhoneNumber == "" { 26 | log.Fatalf("Testing env var TWILIO_DEFAULT_FROM is unset") 27 | } 28 | } 29 | 30 | // Iterate (and paginate) through all the calls 31 | func TestListCalls(t *testing.T) { 32 | iter := TestClient.Calls().Iter() 33 | callCount := 0 34 | var call Call 35 | for iter.Next(&call) { 36 | callCount++ 37 | } 38 | if iter.Err() != nil { 39 | t.Fatalf("error: %s", iter.Err().Error()) 40 | } 41 | t.Logf("Calls total: %d\n", callCount) 42 | } 43 | 44 | // Iterate (and paginate) through all calls from FromPhoneNumber within 45 | // one week 46 | func TestQueryCalls(t *testing.T) { 47 | weekAgo := time.Now().Add(-7 * 24 * time.Hour) 48 | iter := TestClient.Calls( 49 | From(FromPhoneNumber), 50 | StartedAfterYMD(weekAgo)).Iter() 51 | callCount := 0 52 | var call Call 53 | for iter.Next(&call) { 54 | callCount++ 55 | } 56 | if iter.Err() != nil { 57 | t.Fatalf("error: %s", iter.Err().Error()) 58 | } 59 | t.Logf("Within-one-week calls total: %d\n", callCount) 60 | } 61 | 62 | // Iterate (and paginate) through all the messages 63 | func TestListMessages(t *testing.T) { 64 | iter := TestClient.Messages().Iter() 65 | msgCount := 0 66 | var msg Message 67 | for iter.Next(&msg) { 68 | msgCount++ 69 | } 70 | if iter.Err() != nil { 71 | t.Fatalf("error: %s\n", iter.Err().Error()) 72 | } 73 | t.Logf("Messages total: %d\n", msgCount) 74 | } 75 | 76 | // Iterate (and paginate) through all calls from FromPhoneNumber within 77 | // one week 78 | func TestQueryMessages(t *testing.T) { 79 | weekAgo := time.Now().Add(-7 * 24 * time.Hour) 80 | iter := TestClient.Messages( 81 | From(FromPhoneNumber), 82 | SentAfterYMD(weekAgo)).Iter() 83 | msgCount := 0 84 | var msg Message 85 | for iter.Next(&msg) { 86 | msgCount++ 87 | } 88 | if iter.Err() != nil { 89 | t.Fatalf("error: %s\n", iter.Err().Error()) 90 | } 91 | t.Logf("With-one-week Messages total: %d\n", msgCount) 92 | } 93 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | # utwil: Go Utilities for Twilio 2 | # 3 | **utwil is currently under heavy development, so expect breakage.** 4 | 5 | Documentation can be found at: 6 | [https://godoc.org/github.com/wyc/utwil](https://godoc.org/github.com/wyc/utwil) 7 | 8 | Made with [gojson](https://github.com/ChimeraCoder/gojson). 9 | 10 | ## Installation 11 | In your terminal: 12 | ``` bash 13 | $ go get github.com/wyc/utwil 14 | ``` 15 | 16 | In your code: 17 | ``` go 18 | import "github.com/wyc/utwil" 19 | ``` 20 | 21 | ## Features 22 | 23 | ##### Make a utwil.Client 24 | ``` go 25 | client := utwil.NewClient(AccoutSID, AuthToken) 26 | ``` 27 | 28 | ##### Send an SMS 29 | 30 | ``` go 31 | // type client.SendSMS func(from, to, msg string) (utwil.Message, error) 32 | msg, err := client.SendSMS("+15551231234", "+15553214321", "Hello, world!") 33 | ``` 34 | 35 | ##### Send an MMS 36 | 37 | ``` go 38 | // type client.SendMMS func(from, to, msg, mediaURL string) (utwil.Message, error) 39 | mediaURL := "http://i.imgur.com/sZPem77.png" 40 | body := "Hello, world!" 41 | msg, err := client.SendMMS("+15551231234", "+15553214321", body, mediaURL) 42 | ``` 43 | 44 | ##### Make a Call 45 | 46 | ``` go 47 | callbackPostURL := fmt.Sprintf( 48 | "http://twimlets.com/forward?PhoneNumber=%s", 49 | "+15559871234", 50 | ) 51 | // type client.Call func(from, to, callbackPostURL string) (utwil.Call, error) 52 | call, err := client.Call("+15551231234", "+15553214321", callbackPostURL) 53 | ``` 54 | 55 | ##### Make a Recorded Call 56 | 57 | ``` go 58 | // type client.RecordedCall func(from, to, callbackURL string) (utwil.Call, error) 59 | call, err := client.RecordedCall("+15551231234", "+15553214321", callbackPostURL) 60 | ``` 61 | 62 | ##### Lookups 63 | ``` go 64 | // type client.Lookup func(phoneNumber string) (utwil.Lookup, error) 65 | lookup, err := client.Lookup("+15551231234") 66 | // handle err 67 | fmt.Println(lookup.Carrier.Type) // "mobile", "landline", or "voip" 68 | ``` 69 | 70 | ##### Custom requests 71 | For more complicated requests, populate the respective XxxxxReq struct 72 | and call the `client.SubmitXxxxx(XxxxxReq) (Xxxxx, error)` method: 73 | ``` go 74 | msgReq := utwil.MessageReq{ 75 | From: "+15559871234", 76 | To: "+15551231234", 77 | Body: "Hello, world!", 78 | StatusCallback: "https://post.here.com/when/msg/status/changes.twiml", 79 | } 80 | msg, err := client.SubmitMessage(msgReq) 81 | ``` 82 | 83 | ##### Query Messages (SMS/MMS) 84 | 85 | ``` go 86 | weekAgo := time.Now().Add(-7 * 24 * time.Hour) 87 | iter := client.Messages( 88 | utwil.SentBeforeYMD(weekAgo), 89 | utwil.To("+15551231234")).Iter() 90 | var msg utwil.Message 91 | for iter.Next(&msg) { 92 | // do something with utwil.Message 93 | } 94 | if iter.Err() != nil { 95 | // handle err 96 | } 97 | ``` 98 | 99 | ##### Query Calls 100 | ``` go 101 | iter := client.Calls( 102 | utwil.From("+15551231234"), 103 | utwil.StartedAfter("2015-04-01")).Iter() 104 | var call utwil.Call 105 | for iter.Next(&call) { 106 | // do something with utwil.Call 107 | } 108 | if iter.Err() != nil { 109 | // handle err 110 | } 111 | ``` 112 | 113 | ## Testing 114 | First, populate env vars `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, 115 | `TWILIO_DEFAULT_TO`, `TWILIO_DEFAULT_FROM`. 116 | Then run `go test` and expect many annoyances to `TWILIO_DEFAULT_TO`: 117 | - Phone call and second forwarded phone call to the same number 118 | - One SMS message 119 | 120 | Run `go test -test.v` instead if you want more details to the console. 121 | 122 | ## To do 123 | - Better testing harness...maybe we can borrow a testing AccountSID/AuthToken 124 | pair and *commit them to the repo* >:) 125 | - Fetching additional resources from a call/msg such as recording or MMS 126 | - Tools for responding with TwiML, including changing live call state 127 | - CRUD for managerial records such as accounts, addresses, phone numbers, 128 | queues, SIP, etc 129 | - More comments in src 130 | - Investigate STUN, TURN, and ICE offerings 131 | 132 | Feel free to request features by opening an issue. 133 | 134 | ## Alternatives 135 | - https://github.com/sfreiberg/gotwilio (No lookup or call/msg listing) 136 | - https://bitbucket.org/ckvist/twilio (Has tools for generating TwiML, but no lookup or call/msg listing) 137 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "time" 7 | ) 8 | 9 | // Message is the Go-representation of Twilio REST API's message. 10 | // 11 | // Details: 12 | // 13 | // https://www.twilio.com/docs/api/rest/message 14 | // 15 | type Message struct { 16 | AccountSID string `json:"account_sid"` 17 | APIVersion string `json:"api_version"` 18 | Body string `json:"body"` 19 | DateCreated *Time `json:"date_created"` 20 | DateSent *Time `json:"date_sent"` 21 | DateUpdated *Time `json:"date_updated"` 22 | Direction string `json:"direction"` 23 | ErrorCode *int `json:"error_code"` 24 | ErrorMessage *string `json:"error_message"` 25 | From string `json:"from"` 26 | NumMedia string `json:"num_media"` 27 | NumSegments string `json:"num_segments"` 28 | Price string `json:"price"` 29 | PriceUnit string `json:"price_unit"` 30 | SID string `json:"sid"` 31 | Status string `json:"status"` 32 | SubresourceURIs struct { 33 | Media string `json:"media"` 34 | } `json:"subresource_uris"` 35 | To string `json:"to"` 36 | URI string `json:"uri"` 37 | } 38 | 39 | // MessageReq is the Go-representation of Twilio REST API's message request. 40 | // 41 | // Details: 42 | // 43 | // https://www.twilio.com/docs/api/rest/sending-messages 44 | // 45 | type MessageReq struct { 46 | From string 47 | To string 48 | Body string 49 | MediaURL string 50 | StatusCallback string 51 | ApplicationSID string 52 | } 53 | 54 | // SubmitMessage sends a message request populating form fields only if they contain 55 | // a non-zero value. 56 | func (c *Client) SubmitMessage(req MessageReq) (Message, error) { 57 | // @TODO wait until github.com/gorilla/schema supports struct-to-url.Values 58 | values := url.Values{} 59 | values.Set("From", req.From) 60 | values.Set("To", req.To) 61 | values.Set("Body", req.Body) 62 | if req.MediaURL != "" { 63 | values.Set("MediaUrl", req.MediaURL) 64 | } 65 | if req.StatusCallback != "" { 66 | values.Set("StatusCallback", req.StatusCallback) 67 | } 68 | if req.ApplicationSID != "" { 69 | values.Set("ApplicationSid", req.ApplicationSID) 70 | } 71 | var msg Message 72 | err := c.postForm(fmt.Sprintf("%s/Messages.json", c.urlPrefix()), values, &msg) 73 | return msg, err 74 | } 75 | 76 | // SendSMS sends body from/to the specified number. 77 | // 78 | // Example: 79 | // 80 | // msg, err := client.SendSMS("+15551231234", "+15553214321", "Hello, world!") 81 | // 82 | func (c *Client) SendSMS(from, to, body string) (Message, error) { 83 | return c.SendMMS(from, to, body, "") 84 | } 85 | 86 | // SendMMS sends body and mediaURL from/to the specified number. 87 | // 88 | // Example: 89 | // 90 | // mediaURL := "http://i.imgur.com/sZPem77.png" 91 | // body := "Hello, world!" 92 | // msg, err := client.SendMMS("+15551231234", "+15553214321", body, mediaURL) 93 | // 94 | func (c *Client) SendMMS(from, to, body, mediaURL string) (Message, error) { 95 | req := MessageReq{ 96 | From: from, 97 | To: to, 98 | Body: body, 99 | MediaURL: mediaURL, 100 | } 101 | return c.SubmitMessage(req) 102 | } 103 | 104 | // MessageListQuery is a struct that contains an embedded utwil.ListQuery. 105 | // The typing allows the correctly-typed iterator/list to be returned. 106 | type MessageListQuery struct{ *ListQuery } 107 | 108 | // Messages takes a vargs of utwil.ListQueryConf functions to configure the 109 | // query to be sent to the Twilio API: 110 | // 111 | // iter := client.Messages( 112 | // utwil.SentAfter("2014-01-01"), 113 | // utwil.From("+15551231234")).Iter() 114 | // 115 | func (c *Client) Messages(confs ...ListQueryConf) *MessageListQuery { 116 | return &MessageListQuery{ListQuery: newListQuery(c, confs...)} 117 | } 118 | 119 | // SentBefore filters messages sent before a given date string "YYYY-MM-DD" 120 | func SentBefore(ymd string) ListQueryConf { 121 | return func(q *ListQuery) { q.Values.Set("DateSent<", ymd) } 122 | } 123 | 124 | // SentBeforeYMD filters messages sent before a given date (YMD considered only) 125 | func SentBeforeYMD(t time.Time) ListQueryConf { 126 | return SentBefore(t.Format(YMD)) 127 | } 128 | 129 | // SentAfter filters messages sent after a given date string "YYYY-MM-DD" 130 | func SentAfter(ymd string) ListQueryConf { 131 | return func(q *ListQuery) { q.Values.Set("DateSent>", ymd) } 132 | } 133 | 134 | // SentAfterYMD filters messages sent after a given date (YMD considered only) 135 | func SentAfterYMD(t time.Time) ListQueryConf { 136 | return SentAfter(t.Format(YMD)) 137 | } 138 | 139 | // Iter creates an iterator that iterates utwil.Message results 140 | func (q *MessageListQuery) Iter() *MessageIter { 141 | initURI := fmt.Sprintf("%s?%s", q.messagesURL(), q.Values.Encode()) 142 | iter := &MessageIter{iter: newIter(q.Client, initURI)} 143 | iter.iterable = messageList{} 144 | return iter 145 | } 146 | 147 | type messageList struct { 148 | Messages []Message `json:"messages"` 149 | listResource 150 | } 151 | 152 | func (ml messageList) item(idx int) interface{} { 153 | return ml.Messages[idx] 154 | } 155 | 156 | func (ml messageList) size() int { 157 | return len(ml.Messages) 158 | } 159 | 160 | func (ml messageList) nextPage(c *Client) (iterable, error) { 161 | return ml.loadNextPage(c, &messageList{}) 162 | } 163 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | "sync" 8 | ) 9 | 10 | // ListQuery stores query filter configuration and a *utwil.Client, 11 | // and is used by MessageListQuery and CallListQuery. 12 | type ListQuery struct { 13 | url.Values 14 | *Client 15 | } 16 | 17 | // ListQueryConf configures a passed *utwil.ListQuery 18 | type ListQueryConf func(*ListQuery) 19 | 20 | // newListQuery takes a utwil.Client and functional options to create and 21 | // configure a new ListQuery. 22 | // 23 | // Read more about "functional options": 24 | // 25 | // http://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis 26 | // 27 | func newListQuery(c *Client, confs ...ListQueryConf) *ListQuery { 28 | q := &ListQuery{ 29 | Values: make(url.Values), 30 | Client: c, 31 | } 32 | for _, conf := range confs { 33 | conf(q) 34 | } 35 | return q 36 | 37 | } 38 | 39 | // From filters calls and messages sent from a phone number. 40 | func From(phoneNumber string) ListQueryConf { 41 | return func(q *ListQuery) { q.Values.Set("From", phoneNumber) } 42 | } 43 | 44 | // To filters calls and messages sent to a phone number. 45 | func To(phoneNumber string) ListQueryConf { 46 | return func(q *ListQuery) { q.Values.Set("To", phoneNumber) } 47 | } 48 | 49 | type listResource struct { 50 | Page int `json:"page"` 51 | PageSize int `json:"page_size"` 52 | NumPages int `json:"num_pages"` 53 | 54 | Start int `json:"start"` 55 | End int `json:"end"` 56 | Total int `json:"total"` 57 | 58 | URI string `json:"uri"` 59 | PreviousPageURI *string `json:"previous_page_uri"` 60 | NextPageURI *string `json:"next_page_uri"` 61 | FirstPageURI string `json:"first_page_uri"` 62 | LastPageURI string `json:"last_page_uri"` 63 | } 64 | 65 | func (lr listResource) nextPageFullURI() string { 66 | return fmt.Sprintf("%s%s", BaseURL, *lr.NextPageURI) 67 | } 68 | 69 | func (lr listResource) loadNextPage(c *Client, result iterable) (iterable, error) { 70 | err := c.getJSON(lr.nextPageFullURI(), result) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return result, nil 75 | } 76 | 77 | func (lr listResource) hasNextPage() bool { 78 | return lr.NextPageURI != nil && *lr.NextPageURI != "" 79 | } 80 | 81 | type iterable interface { 82 | item(idx int) interface{} 83 | size() int 84 | 85 | hasNextPage() bool 86 | nextPage(*Client) (iterable, error) 87 | } 88 | 89 | type iter struct { 90 | m sync.Mutex 91 | err error 92 | iterable iterable 93 | pageItem int 94 | didInit bool 95 | initURI string 96 | client *Client 97 | } 98 | 99 | func newIter(c *Client, initURI string) *iter { 100 | return &iter{ 101 | m: sync.Mutex{}, 102 | err: nil, 103 | iterable: nil, 104 | pageItem: 0, 105 | didInit: false, 106 | initURI: initURI, 107 | client: c, 108 | } 109 | } 110 | 111 | func (iter *iter) loadInitURI() error { 112 | if iter.iterable == nil { 113 | panic("iterable uninitalized") 114 | } else if iter.initURI == "" { 115 | return fmt.Errorf("initURI uninitialized") 116 | } 117 | 118 | err := iter.client.getJSON(iter.initURI, iter.iterable) 119 | if err != nil { 120 | return err 121 | } 122 | return nil 123 | } 124 | 125 | func (iter *iter) next(result interface{}) bool { 126 | iter.m.Lock() 127 | defer iter.m.Unlock() 128 | 129 | if !iter.didInit { 130 | err := iter.loadInitURI() 131 | if err != nil { 132 | iter.err = err 133 | return false 134 | } 135 | iter.pageItem = 0 136 | iter.didInit = true 137 | } 138 | 139 | if iter.pageItem == iter.iterable.size() { 140 | if !iter.iterable.hasNextPage() { 141 | return false 142 | } 143 | nextIter, err := iter.iterable.nextPage(iter.client) 144 | if err != nil { 145 | iter.err = err 146 | return false 147 | } 148 | iter.pageItem = 0 149 | iter.iterable = nextIter 150 | } 151 | 152 | item := iter.iterable.item(iter.pageItem) 153 | lValuePtr := reflect.ValueOf(result) 154 | lValue := reflect.Indirect(lValuePtr) 155 | rValue := reflect.ValueOf(item) 156 | if lValue.Type() != rValue.Type() { 157 | panic(fmt.Sprintf("Iter.next() tried to load %s into %s", 158 | rValue.Type(), lValue.Type())) 159 | } 160 | lValue.Set(rValue) 161 | iter.pageItem++ 162 | return true 163 | } 164 | 165 | // Err returns the latest error a MessageIter or CallIter encounters or nil 166 | // if there was no error. 167 | func (iter *iter) Err() error { return iter.err } 168 | 169 | // MessageIter iterates through Twilio messages. 170 | type MessageIter struct{ *iter } 171 | 172 | // Next attempts to populate msg with the next utwil.Message, returning false 173 | // if it could not due to out of messages or an error. It is therefore 174 | // recommended to check for errors with MessageIter.Err() after use: 175 | // 176 | // Example: 177 | // 178 | // var msg utwil.Message 179 | // for messageIter.Next(&msg) { 180 | // // use msg 181 | // } 182 | // if messageIter.Err() != nil { 183 | // // handle err 184 | // } 185 | // 186 | func (iter *MessageIter) Next(msg *Message) bool { return iter.next(msg) } 187 | 188 | // CallIter iterates through Twilio calls. 189 | type CallIter struct{ *iter } 190 | 191 | // Next attempts to populate call with the next utwil.Call, returning false 192 | // if it could not due to out of messages or an error. It is therefore 193 | // recommended to check for errors with CallIter.Err() after use: 194 | // 195 | // Example: 196 | // 197 | // var call utwil.Call 198 | // for callIter.Next(&call) { 199 | // // use msg 200 | // } 201 | // if callIter.Err() != nil { 202 | // // handle err 203 | // } 204 | // 205 | func (iter *CallIter) Next(call *Call) bool { return iter.next(call) } 206 | -------------------------------------------------------------------------------- /call.go: -------------------------------------------------------------------------------- 1 | package utwil 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | // Call is the Go-representation of Twilio REST API's call. 11 | // 12 | // Details: 13 | // 14 | // https://www.twilio.com/docs/api/rest/call 15 | // 16 | type Call struct { 17 | AccountSID string `json:"account_sid"` 18 | Annotation string `json:"annotation"` 19 | AnsweredBy string `json:"answered_by"` 20 | ApiVersion string `json:"api_version"` 21 | CallerName string `json:"caller_name"` 22 | DateCreated *Time `json:"date_created"` 23 | DateUpdated *Time `json:"date_updated"` 24 | Direction string `json:"direction"` 25 | Duration string `json:"duration"` 26 | ForwardedFrom string `json:"forwarded_from"` 27 | From string `json:"from"` 28 | FromFormatted string `json:"from_formatted"` 29 | GroupSID string `json:"group_sid"` 30 | ParentCallSID string `json:"parent_call_sid"` 31 | PhoneNumberSID string `json:"phone_number_sid"` 32 | Price string `json:"price"` 33 | PriceUnit string `json:"price_unit"` 34 | SID string `json:"sid"` 35 | StartTime *Time `json:"start_time"` 36 | EndTime *Time `json:"end_time"` 37 | Status string `json:"status"` 38 | SubresourceURIs struct { 39 | Notifications string `json:"notifications"` 40 | Recordings string `json:"recordings"` 41 | } `json:"subresource_uris"` 42 | To string `json:"to"` 43 | ToFormatted string `json:"to_formatted"` 44 | URI string `json:"uri"` 45 | } 46 | 47 | // CallReq is the Go-representation of the Twilio REST API's call request. 48 | // 49 | // Details: 50 | // 51 | // https://www.twilio.com/docs/api/rest/making-calls 52 | // 53 | type CallReq struct { 54 | From string 55 | To string 56 | URL string 57 | ApplicationSID string 58 | Method string 59 | FallbackURL string 60 | FallbackMethod string 61 | StatusCallback string 62 | StatusCallbackMethod string 63 | SendDigits string 64 | IfMachine string 65 | Timeout int 66 | Record bool 67 | } 68 | 69 | // SubmitCall sends a call request populating form fields only if they contain 70 | // a non-zero value. 71 | func (c *Client) SubmitCall(req CallReq) (*Call, error) { 72 | // @TODO wait until github.com/gorilla/schema supports struct-to-url.Values 73 | values := url.Values{} 74 | values.Set("From", req.From) 75 | values.Set("To", req.To) 76 | if req.URL != "" { 77 | values.Set("Url", req.URL) 78 | } 79 | if req.ApplicationSID != "" { 80 | values.Set("ApplicationSid", req.ApplicationSID) 81 | } 82 | if req.Method != "" { 83 | values.Set("Method", req.Method) 84 | } 85 | if req.FallbackURL != "" { 86 | values.Set("FallbackUrl", req.FallbackURL) 87 | } 88 | if req.FallbackMethod != "" { 89 | values.Set("FallbackMethod", req.FallbackMethod) 90 | } 91 | if req.StatusCallback != "" { 92 | values.Set("StatusCallback", req.StatusCallback) 93 | } 94 | if req.StatusCallbackMethod != "" { 95 | values.Set("StatusCallbackMethod", req.StatusCallbackMethod) 96 | } 97 | if req.SendDigits != "" { 98 | values.Set("SendDigits", req.SendDigits) 99 | } 100 | if req.IfMachine != "" { 101 | values.Set("IfMachine", req.IfMachine) 102 | } 103 | if req.Timeout > 0 { 104 | values.Set("Timeout", strconv.Itoa(req.Timeout)) 105 | } 106 | if req.Record { 107 | values.Set("Record", "true") 108 | } 109 | call := &Call{} 110 | err := c.postForm(fmt.Sprintf("%s/Calls.json", c.urlPrefix()), values, call) 111 | return call, err 112 | } 113 | 114 | // Call requests the Twilio API to call a number and send a POST request to 115 | // the given URL to report what happened: 116 | // 117 | // https://www.twilio.com/docs/api/twiml/twilio_request 118 | // 119 | // Example: 120 | // 121 | // callbackPostURL := fmt.Sprintf( 122 | // "http://twimlets.com/forward?PhoneNumber=%s", 123 | // "+15559871234", 124 | // ) 125 | // call, err := client.Call("+15551231234", "+15553214321", callbackPostURL) 126 | // 127 | func (c *Client) Call(from, to, callbackPostURL string) (*Call, error) { 128 | req := CallReq{ 129 | From: from, 130 | To: to, 131 | URL: callbackPostURL, 132 | } 133 | return c.SubmitCall(req) 134 | } 135 | 136 | // RecordedCall is the same as Client.Call, but recorded 137 | func (c *Client) RecordedCall(from, to, callbackPostURL string) (*Call, error) { 138 | req := CallReq{ 139 | From: from, 140 | To: to, 141 | URL: callbackPostURL, 142 | Record: true, 143 | } 144 | return c.SubmitCall(req) 145 | } 146 | 147 | // CallListQuery is a struct that contains an embedded utwil.ListQuery. 148 | // The typing allows the correctly-typed iterator/list to be returned. 149 | type CallListQuery struct{ *ListQuery } 150 | 151 | // Calls takes a vargs of utwil.ListQueryConf functions to configure the query 152 | // to be sent to the Twilio API: 153 | // 154 | // Example: 155 | // 156 | // iter := client.Calls( 157 | // utwil.StartedBefore("2014-01-01"), 158 | // utwil.To("+15551231234")).Iter() 159 | // 160 | func (c *Client) Calls(confs ...ListQueryConf) *CallListQuery { 161 | return &CallListQuery{ListQuery: newListQuery(c, confs...)} 162 | } 163 | 164 | // StartedBefore filters calls started before a given date string "YYYY-MM-DD" 165 | func StartedBefore(ymd string) ListQueryConf { 166 | return func(q *ListQuery) { q.Values.Set("StartTime<", ymd) } 167 | } 168 | 169 | // StartedBeforeYMD filters calls started before a given date (YMD considered only) 170 | func StartedBeforeYMD(t time.Time) ListQueryConf { 171 | return StartedBefore(t.Format(YMD)) 172 | } 173 | 174 | // StartedAfter filters calls started after a given date string "YYYY-MM-DD" 175 | func StartedAfter(ymd string) ListQueryConf { 176 | return func(q *ListQuery) { q.Values.Set("StartTime>", ymd) } 177 | } 178 | 179 | // StartedAfterYMD filters calls started after a given date (YMD considered only) 180 | func StartedAfterYMD(t time.Time) ListQueryConf { 181 | return StartedAfter(t.Format(YMD)) 182 | } 183 | 184 | // Iter creates an iterator that iterates utwil.Call results 185 | func (q *CallListQuery) Iter() *CallIter { 186 | initURI := fmt.Sprintf("%s?%s", q.callsURL(), q.Values.Encode()) 187 | iter := &CallIter{iter: newIter(q.Client, initURI)} 188 | iter.iterable = &callList{} 189 | return iter 190 | } 191 | 192 | type callList struct { 193 | Calls []Call `json:"calls"` 194 | listResource 195 | } 196 | 197 | func (cl callList) item(idx int) interface{} { return cl.Calls[idx] } 198 | func (cl callList) size() int { return len(cl.Calls) } 199 | func (cl callList) nextPage(c *Client) (iterable, error) { 200 | return cl.loadNextPage(c, &callList{}) 201 | } 202 | --------------------------------------------------------------------------------