twilio-rb 0.4 → 1.0beta

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
 
2
2
  # Twilio.rb
3
3
 
4
- Interact with the Twilio API in a nice Ruby way
4
+ Interact with the Twilio API in a nice Ruby way.
5
+
6
+ Twilio.rb is the only library that encapsulates Twilio resources as Ruby objects, has 100% test coverage, and supports the whole API.
7
+
8
+ It offers an ActiveRecord style API, i.e. one that most Ruby developers are familiar using to manipulate Ruby objects with.
5
9
 
6
10
  ## Installation
7
11
 
@@ -15,6 +19,10 @@ Require the library in your script as
15
19
 
16
20
  <pre>require 'twilio'</pre>
17
21
 
22
+ or using bundler:
23
+
24
+ <pre>gem 'twilio-rb', '1.0pre', :require => 'twilio'</pre>
25
+
18
26
  ## Configuration
19
27
 
20
28
  Configuration for this library is encapsulated within `Twilio::Config`. One needs to setup with an Account SID and an Auth Token, e.g.
@@ -27,50 +35,19 @@ end</pre>
27
35
 
28
36
  Any method that calls the Twilio API will raise `Twilio::ConfigurationError` if either Account SID or Auth Token are not configured.
29
37
 
30
- ## The Account object
31
-
32
- The Twilio API in its current incarnation supports one Twilio account per Account SID, and so the Twilio::Account object correspondingly is a singleton object.
33
-
34
- To access properties of the account the property name should be called as a method on the object itself, e.g.
35
-
36
- <pre>Twilio::Account.friendly_name</pre>
37
-
38
- The first time a method is invoked on the object an API call is made to retrieve the data. The methods themselves are not defined until they are called, i.e. lazy evaluation. This strategy means that addtional properties added to subsequent versions of the API should not break the library.
39
-
40
- To reload the data when needed `Twilio::Account.reload!` will make another API call and update its own internal state.
41
-
42
- Predicate methods i.e. those ending in `?` map directly to the status of the account, e.g. `Twilio::Account.suspended?` returns true if Twilio have suspended your account. Again, all of these methods are defined on the fly.
43
-
44
- The only account property that can be modified via the REST API is the friendly name, e.g.
45
-
46
- <pre>Twilio::Account.friendly_name = "I'm so vain, I had to change my name!"</pre>
47
-
48
- This will update the API immediately with a PUT request.
49
-
50
- Please refer to the Twilio REST API documentation for an up to date list of properties.
38
+ # Getting started
51
39
 
52
40
  ## Making a telephone call
53
41
 
54
42
  The API used to make a telephone call is similar to interacting with an ActiveRecord model object.
55
43
  <pre>Twilio::Call.create :to => '+16465551234', :from => '+19175550000',
56
44
  :url => "http://example.com/call_handler"</pre>
57
- or
58
- <pre>call = Twilio::Call.new :to => '+16465551234', :from => '+19175550000',
59
- :url => "http://example.com/call_handler"
60
-
61
- call.save</pre>
62
45
 
63
46
  The parameter keys should be given as underscored symbols. They will be converted internally to camelized strings prior to an API call being made.
64
47
 
65
48
  Please see the Twilio REST API documentation for an up to date list of supported parameters.
66
49
 
67
- ## Finding an existing telephone call
68
-
69
- To retrieve an earlier created call, there is the `Twilio::Call.find` method, which accepts a call SID, e.g.
70
-
71
- <pre>call = Twilio::Call.find 'CAa346467ca321c71dbd5e12f627deb854'</pre>
72
-
73
- This returns an instance of `Twilio::Call` if a call with the given SID was found, otherwise nil is returned
50
+ If the request was successful, an instance of `Twilio::Call` wil be returned
74
51
 
75
52
  ### Modifying a live telephone call
76
53
 
@@ -83,21 +60,26 @@ Once a call has been been created it can be modified with the following methods:
83
60
  `Twilio::Call#cancel!` and `Twilio::Call#complete!` will raise `Twilio::InvalidStateError` if the call has not been "saved".
84
61
  `Twilio::Call#url=` will updated its state with the new URL ready for when `Twilio::Call#save` is called.
85
62
 
63
+ ## Finding an existing telephone call
64
+
65
+ To retrieve an earlier created call, there is the `Twilio::Call.find` method, which accepts a call SID, e.g.
66
+
67
+ <pre>call = Twilio::Call.find 'CAa346467ca321c71dbd5e12f627deb854'</pre>
68
+
69
+ This returns an instance of `Twilio::Call` if a call with the given SID was found, otherwise nil is returned
70
+
86
71
  ## Sending an SMS message
87
72
 
88
73
  The API used to send an SMS message is similar to interacting with an ActiveRecord model object.
89
74
  <pre>Twilio::SMS.create :to => '+16465551234', :from => '+19175550000',
90
75
  :body => "Hey baby, how was your day? x"</pre>
91
- or
92
- <pre>sms = Twilio::SMS.new :to => '+16465551234', :from => '+19175550000',
93
- :body => "Hey baby, how was your day? x"
94
-
95
- sms.save</pre>
96
76
 
97
77
  The parameter keys should be given as underscored symbols. They will be converted internally to camelized strings prior to an API call being made.
98
78
 
99
79
  Please see the Twilio REST API documentation for an up to date list of supported parameters.
100
80
 
81
+ If the request was successful, an instance of `Twilio::SMS` wil be returned
82
+
101
83
  ## Finding an existing telephone SMS message
102
84
 
103
85
  To retrieve an earlier created SMS message, there is the `Twilio::SMS.find` method, which accepts a SMS message SID, e.g.
@@ -106,7 +88,7 @@ To retrieve an earlier created SMS message, there is the `Twilio::SMS.find` meth
106
88
 
107
89
  This returns an instance of `Twilio::SMS` if a SMS message with the given SID was found, otherwise nil is returned
108
90
 
109
- ## Building TwiML documents
91
+ # Building TwiML documents
110
92
 
111
93
  A TwiML document is an XML document. The best way to build XML in Ruby is with Builder, and so it follows that we should use builder for TwiML. `Twilio::TwiML.build` behaves like builder except element names are capitalised for you and attributes are camelized for you as well. This is so you may continue to write beautiful code.
112
94
 
@@ -143,3 +125,190 @@ Therefore emits the following TwiML document:
143
125
  </pre>
144
126
 
145
127
  This specialised behaviour only affects `Twilio::TwiML.build` and does not affect Builder in general.
128
+
129
+ # Rails 3 integration
130
+
131
+ Twilio.rb has Rails integration out of the box. It adds a new mime type :voice and a template handler for TwiML views.
132
+ So now your Rails app can respond_to :voice. Insane!
133
+
134
+ <pre>
135
+ class FooController &lt; ApplicationController
136
+ responds_to :html, :voice
137
+
138
+ def index
139
+ ...
140
+ end
141
+ end
142
+ </pre>
143
+
144
+ coupled with the following view file `app/views/foo/index.voice`
145
+
146
+ <pre>
147
+ res.say 'Damn this library is so ill dude!'
148
+ </pre>
149
+
150
+ It's now easier than ever to integrate Twilio in your Rails app cleanly and easily.
151
+
152
+ # Basics
153
+
154
+ Each super-resource, e.g. Calls, OutgoingCallerIds, etc has a Ruby object in the Twilio namespace representing it, Twilio::Call, Twilio::OutgoingCallerId, etc.
155
+
156
+ ## Summary
157
+
158
+ Resources that can be created via the API, using the HTTP POST verb can be done so in the library using the `.create` class method, e.g.
159
+
160
+ <pre>Twilio::Call.create :to => '+16465551234', :from => '+19175550000',
161
+ :url => "http://example.com/call_handler"</pre>
162
+
163
+ Resources that can be removed via the API, using the HTTP DELETE verb can be done so in the library using the `#destroy` instance method, e.g.
164
+
165
+ <pre>
166
+ # Delete all log entries
167
+ Twilio::Notification.all.each &:destroy
168
+ </pre>
169
+
170
+ Object representations instantiated by the library respond to all methods that match attributes on its corresponding resource. The method names are those of the parameters in snake case (underscored), i.e. as they are returned in the JSON representation.
171
+
172
+ The Twilio API documentation itself is the canonical reference for which resources have what properties, and which of those can be updated by the API. Please refer to the Twilio REST API documentation for thos information.
173
+
174
+ ## Accessing resource instances
175
+
176
+ Resource instances can be accessed ad hoc passsing the resource sid to the `.find` class method on the resource class, e.g.
177
+
178
+ <pre>call = Twilio::Call.find 'CAe1644a7eed5088b159577c5802d8be38'</pre>
179
+
180
+ This will return an instance of the resource class, in this case `Twilio::Call`, with the attributes of the resource. These attributes are accessed using dynamically defined getter methods, where the method name is the attribute name underscored, i.e. as they are returned in a JSON response from the API.
181
+
182
+ Sometimes these method name might collide with native Ruby methods, one such example is the `method` parameter colliding with `Object#method`. Native Ruby methods are never overridden by the library as they are lazily defined using `method_missing`. To access these otherwise unreachable attributes, there is another syntax for accessing resource attributes:
183
+
184
+
185
+ <pre>
186
+ call = Twilio::Call.find 'CAe1644a7eed5088b159577c5802d8be38'
187
+ call[:method] # With a symbol or
188
+ call['method'] # or with a string. Access is indifferent.
189
+ </pre>
190
+
191
+ ## Querying list resources
192
+
193
+ List resources can be accessed ad hoc by calling the `.all` class method on the resource class, e.g.
194
+
195
+ <pre>calls = Twilio::Call.all</pre>
196
+
197
+ This will return a collection of objects, each a representation of the corresponding resource.
198
+
199
+ ### Using filter parameters to refine a query
200
+
201
+ The `.all` class method will ask Twilio for all resource instances on that list resource, this can easily result in a useless response if there are numerous resource instances on a given resource. The `.all` class method accepts a hash of options for parameters to filter the response, e.g.
202
+
203
+ <pre>Twilio::Call.all :to => '+19175550000', :status => 'complete'</pre>
204
+
205
+ Twilio does some fancy stuff to implement date ranges, consider the API request:
206
+
207
+ <pre>GET /2010-04-01/Accounts/AC5ef87.../Calls?StartTime&gt;=2009-07-06&EndTime&lt;=2009-07-10</pre>
208
+
209
+ This will return all calls started after midnight July 06th 2009 and completed before July 10th 2009. Any call started and ended within that time range matches those criteria and will be returned. To make the same reqest using this library:
210
+
211
+ <pre>
212
+ require 'date'
213
+ start_date, end_date = Date.parse('2009-07-06'), Date.parse('2009-07-10')
214
+
215
+ Twilio::Call.all :started_after => start_date, :ended_before => end_date
216
+ </pre>
217
+
218
+ All option parameters pertaining to dates accept a string or any object that returns a RFC 2822 date when `#to_s` is called on it, e.g. an instance of `Date`. If a date parameter is not a rnage but absolute, one can of course use the normal option, e.g.
219
+
220
+ <pre>Twilio::Call.all :start_time => start_date</pre>
221
+
222
+ The key names for these Date filters are inconsistent across resources, in the library they are:
223
+
224
+ <pre>
225
+ Twilio::SMS.all :created_before => date, :created_after => date, :sent_before => date, :sent_after # :"created_#{when}" and :"sent_#{when}" are synonomous
226
+ Twilio::Notification.all :created_before => date, :created_after => date
227
+ Twilio::Call.all :started_before => date, :started_after => date, :ended_before => date, :ended_after => date
228
+ </pre>
229
+
230
+ ### Pagination
231
+
232
+ The Twilio API paginates API responses and by default it will return 30 objects in one response, this can be overridden to return up to a maximum of 1000 per response using the `:page_size` option, If more than 1000 resources instances exist, the `:page` option is available, e.g.
233
+
234
+ <pre>Twilio::Call.all :started_after => start_date, :ended_before => end_date, :page_size => 1000, :page => 7</pre>
235
+
236
+ To determine how many resources exist, the `.count` class method exists, which accepts the same options as `.all` in order to constrain the query e.g.
237
+
238
+ <pre>Twilio::Call.count :started_after => start_date, :ended_before => end_date</pre>
239
+
240
+ It returns an integer corresponding to how many resource instances exist with those conditions.
241
+
242
+ ## Updating resource attributes
243
+
244
+ Certain resources have attributes that can be updated with the REST API. Instances of those resources can be updated using either a setter method with a name that corresponds to the attribute, or by using the `#update_attributes`.
245
+
246
+ <pre>
247
+ call = Twilio::Call.all(:status => 'in-progress').first
248
+
249
+ call.url = 'http://example.com/in_ur_apiz_hijackin_ur_callz.xml'
250
+ call.update_attributes :url => 'http://example.com/in_ur_apiz_hijackin_ur_callz.xml'
251
+ </pre>
252
+
253
+ These are both equivalent, i.e. they immediately make an API request and update the state of the object with the API response. The first one in fact uses the second one internally and is just a shortcut. Use the second when there is more than one attribute to be updated in the same HTTP request.
254
+
255
+ ## Manipulating conference participants
256
+
257
+ The participants list resource is a subresource of a conference resource instance:
258
+
259
+ <pre>conference = Twilio::Conference.find 'CFbbe46ff1274e283f7e3ac1df0072ab39'</pre>
260
+
261
+ Conference participants are accessed via the `#particpants` instance method, e.g.
262
+
263
+ <pre>participants = conference.participants</pre>
264
+
265
+ The muted state can be toggled using the `#mute!` instance method, e.g. toggle the mute state off all participants:
266
+
267
+ <pre>participants.each &:mute!</pre>
268
+
269
+ Participants can be removed from the conference using the '#destroy instance method'
270
+
271
+ # Singleton resources
272
+
273
+ The Twilio API in its current incarnation has two singleton (scoped per account) resources, correspondingly there are the Twilio::Account, and Twilio::Sandbox singleton objects.
274
+
275
+ To access properties of a singleton object the property name should be called as a method on the object itself, e.g.
276
+
277
+ <pre>Twilio::Account.friendly_name</pre>
278
+
279
+ The first time a method is invoked on the object an API call is made to retrieve the data. The methods themselves are not defined until they are called, i.e. lazy evaluation. This strategy means that addtional properties added to subsequent versions of the API should not break the library.
280
+
281
+ To reload the data when needed `Twilio::Account.reload!` will make another API call and update its own internal state.
282
+
283
+ The account has a predicate method i.e. ending in `?`, it maps directly to the status of the account, e.g. `Twilio::Account.suspended?` returns true if Twilio have suspended your account. Again, this is defined on the fly.
284
+
285
+ The only account property that can be modified via the REST API is the friendly name, e.g.
286
+
287
+ <pre>Twilio::Account.friendly_name = "I'm so vain, I had to change my name!"</pre>
288
+ <pre>Twilio::Account.update_attributes :friendly_name => "I'm so vain, I had to change my name!"</pre>
289
+
290
+ This will update the API immediately with a POST request.
291
+
292
+ Twilio::Sandbox supports more options. Please refer to the Twilio REST API documentation for an up to date list of properties.
293
+
294
+ # Searching for and purchasing available phone numbers
295
+
296
+ The Twilio API allows a user to search for available phone numbers in Twilio's inventory. e.g. to find a number in the 917 area code containing '7777':
297
+
298
+ <pre>Twilio::AvailablePhoneNumber.all :area_code => '917', :contains => '7777'</pre>
299
+
300
+ A collection of Twilio::AvailablePhoneNumber objects will be returned. e.g. To purchase the first one:
301
+
302
+ <pre>numbers = Twilio::AvailablePhoneNumber.all :area_code => '917', :contains => '7777'
303
+ numbers.first.purchase! :voice_url => 'http://example.com/twiml.xml'</pre>
304
+
305
+ Which is a shortcut for:
306
+
307
+ <pre>numbers = Twilio::AvailablePhoneNumber.all :area_code => '917', :contains => '7777'
308
+ Twilio::IncomingPhoneNumber.create :phone_number => numbers.first.phone_number, :voice_url => 'http://example.com/twiml.xml'</pre>
309
+
310
+ # Recordings
311
+
312
+ A recording resource instance has an extra instance method: `#mp3` this returns a publicly accessible URL for the MP3 representation of the resource instance.
313
+
314
+
data/lib/railtie.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Twilio
2
+ class Railtie < Rails::Railtie
3
+ initializer 'twilio.initialize' do |app|
4
+ module ActionView
5
+ class Template
6
+ module Handlers
7
+ class TwiML < ::ActionView::Template::Handler
8
+ include ::ActionView::Template::Handlers::Compilable
9
+
10
+ def compile(template)
11
+ %<Twilio::TwiML.build { |res| #{template.source} }>
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ ::ActionView::Template.register_template_handler(:voice, ActionView::Template::Handlers::TwiML)
19
+ ::Mime::Type.register_alias 'text/xml', :voice
20
+ end
21
+ end
22
+ end
@@ -1,20 +1,25 @@
1
1
  module Twilio
2
2
  module Account
3
3
  include Twilio::Resource
4
- @attributes = {}
4
+ @attributes = {}.with_indifferent_access
5
5
 
6
6
  def attributes
7
7
  @attributes.empty? ? reload! : @attributes
8
8
  end
9
9
 
10
10
  def reload!
11
- handle_response get "/Accounts/#{Twilio::ACCOUNT_SID}.json"
11
+ handle_response get path
12
12
  end
13
13
 
14
14
  def friendly_name=(name)
15
- handle_response put "/Accounts/#{Twilio::ACCOUNT_SID}.json", :body => { :friendly_name => name }
15
+ update_attributes :friendly_name => name
16
+ end
17
+
18
+ private
19
+ def path
20
+ "/Accounts/#{Twilio::ACCOUNT_SID}.json"
16
21
  end
17
22
 
18
23
  extend self
19
24
  end
20
- end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Twilio
2
+ class AvailablePhoneNumber
3
+ include Twilio::Resource
4
+ extend Twilio::Finder
5
+
6
+ class << self
7
+ def all(opts={})
8
+ opts = Hash[opts.map { |k,v| [k.to_s.camelize, v]}]
9
+ country_code = opts['IsoCountryCode'] ? opts.delete('IsoCountryCode') : 'US'
10
+ number_type = opts.delete('TollFree') ? 'TollFree' : 'Local'
11
+ params = { :query => opts } if opts.any?
12
+
13
+ handle_response get "/Accounts/#{Twilio::ACCOUNT_SID}/AvailablePhoneNumbers/#{country_code}/#{number_type}.json", params
14
+ end
15
+
16
+ private :new
17
+ undef_method :count
18
+
19
+ end
20
+
21
+ # Shortcut for creating a new incoming phone number. Delegates to Twilio::IncomingPhoneNumber.create accepting the same options as that method does.
22
+ def purchase!(opts={})
23
+ Twilio::IncomingPhoneNumber.create opts.update :phone_number => phone_number
24
+ end
25
+ end
26
+ end
data/lib/twilio/call.rb CHANGED
@@ -1,62 +1,49 @@
1
1
  module Twilio
2
2
  class Call
3
3
  include Twilio::Resource
4
+ include Twilio::Persistable
5
+ extend Twilio::Finder
6
+
7
+ class << self
8
+ alias old_create create
9
+ def create(attrs={})
10
+ attrs = attrs.with_indifferent_access
11
+ attrs.each { |k,v| v.upcase! if k.to_s =~ /method$/ }
12
+ attrs[:send_digits] = CGI.escape(attrs[:send_digits]) if attrs[:send_digits]
13
+ attrs['if_machine'].try :capitalize
14
+ old_create attrs
15
+ end
4
16
 
5
- def initialize(attrs ={}) #:nodoc:
6
- @attributes = Hash[attrs.map { |k,v| [k.to_s.camelize, v.to_s] }]
7
- normalize_http_verbs!
8
- escape_send_digits! if attributes.include? 'SendDigits'
9
- normalize_if_machine_parameter!
10
- end
11
-
12
- # Dials the call
13
- def save
14
- handle_response self.class.post "/Accounts/#{Twilio::ACCOUNT_SID}/Calls.json", :body => attributes
17
+ private
18
+ def prepare_dates(opts)
19
+ opts.map do |k,v|
20
+ if [:started_before, :started_after, :ended_before, :ended_after].include? k
21
+ k = k.to_s
22
+ # Fancy schmancy-ness to handle Twilio <= URI operator for dates
23
+ comparator = k =~ /before$/ ? '<=' : '>='
24
+ delineator = k =~ /started/ ? 'Start' : 'End'
25
+ delineator << "Time" << comparator << v.to_s
26
+ else
27
+ "#{k.to_s.camelize}=#{v}"
28
+ end
29
+ end
30
+ end
15
31
  end
16
32
 
17
- # Cancels a call if its state is 'queued' or 'ringing'
33
+ # Cancels a call if its state is 'queued' or 'ringing'
18
34
  def cancel!
19
- state_guard { modify_call 'Status' => 'cancelled' }
35
+ update_attributes :status => 'cancelled'
20
36
  end
21
-
37
+
22
38
  def complete!
23
- state_guard { modify_call 'Status' => 'completed' }
39
+ update_attributes :status => 'completed'
24
40
  end
25
-
41
+
26
42
  # Update Handler URL
27
43
  def url=(url)
28
44
  # If this attribute exists it is assumed the API call to create a call has been made, so we need to tell Twilio.
29
- modify_call "url" => url if self[:status]
45
+ update_attributes :url => url if self[:status]
30
46
  self[:url] = url
31
47
  end
32
-
33
- private
34
-
35
- def normalize_http_verbs! #:nodoc:
36
- # Twilio accepts a HTTP method for use with various callbacks. The API documentation
37
- # indicates that the HTTP verbs are to be passed as upcase.
38
- attributes.each { |k,v| v.upcase! if k =~ /Method$/ }
39
- end
40
-
41
- def escape_send_digits! #:nodoc:
42
- # A pound, i.e. "#" has special meaning in a URL so it must be escaped
43
- attributes.update 'SendDigits' => CGI.escape(attributes['SendDigits'])
44
- end
45
-
46
- def normalize_if_machine_parameter! #:nodoc:
47
- attributes['IfMachine'].capitalize! if attributes['IfMachine']
48
- end
49
-
50
- def state_guard(&blk)
51
- if self[:status] # If this attribute exists it is assumed the API call to create a call has been made, and the object is in the correct state to make request.
52
- blk.call
53
- else
54
- raise Twilio::InvalidStateError.new 'Call is in invalid state to perform this action.'
55
- end
56
- end
57
-
58
- def modify_call(params)
59
- handle_response self.class.post "/Accounts/#{Twilio::ACCOUNT_SID}/Calls/#{self[:sid]}.json", :body => params
60
- end
61
48
  end
62
- end
49
+ end
@@ -0,0 +1,18 @@
1
+ module Twilio
2
+ class Conference
3
+ include Twilio::Resource
4
+ extend Twilio::Finder
5
+
6
+ def participants
7
+ res = self.class.get "/Accounts/#{Twilio::ACCOUNT_SID}/Conferences/#{sid}/Participants.json"
8
+ if (400..599).include? res.code
9
+ raise Twilio::APIError.new "Error ##{res.parsed_response['code']}: #{res.parsed_response['message']}"
10
+ else
11
+ res.parsed_response['participants'].map do |p|
12
+ p['api_version'] = p['api_version'].to_s # api_version parsed as a date by http_party
13
+ Twilio::Participant.new p
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module Twilio
2
+ module Deletable
3
+ def destroy
4
+ state_guard { freeze && true if self.class.delete path }
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,60 @@
1
+ module Twilio
2
+ module Finder
3
+ def find(id)
4
+ res = get "/Accounts/#{Twilio::ACCOUNT_SID}/#{resource_fragment}/#{id}.json"
5
+ hash = res.parsed_response
6
+ if (200..299).include? res.code
7
+ hash['api_version'] = hash['api_version'].to_s # api_version parsed as a date by http_party
8
+ new hash
9
+ end
10
+ end
11
+
12
+ def count(opts={})
13
+ opts = prepare_dates opts
14
+ params = "?#{URI.encode(opts.join '&')}" unless opts.empty?
15
+
16
+ get("/Accounts/#{Twilio::ACCOUNT_SID}/#{resource_fragment}.json#{params}").parsed_response['total']
17
+ end
18
+
19
+ def all(opts={})
20
+ opts = prepare_dates opts
21
+ params = "?#{URI.encode(opts.join '&')}" unless opts.empty?
22
+
23
+ handle_response get "/Accounts/#{Twilio::ACCOUNT_SID}/#{resource_fragment}.json#{params}"
24
+ end
25
+
26
+ private
27
+
28
+ def resource_fragment # :nodoc:
29
+ # All Twilio resources follow a convention, except SMS :(
30
+ klass_name = name.demodulize
31
+ resource = klass_name == 'SMS' ? "#{klass_name}/Messages" : klass_name.pluralize
32
+ end
33
+
34
+ def prepare_dates(opts) # :nodoc:
35
+ opts.map do |k,v|
36
+ if [:updated_before, :created_before, :updated_after, :created_after].include? k
37
+ k = k.to_s
38
+ # Fancy schmancy-ness to handle Twilio <= URI operator for dates
39
+ comparator = k =~ /before$/ ? '<=' : '>='
40
+ "Date" << k.gsub(/\_\w+$/, '').capitalize << comparator << v.to_s
41
+ else
42
+ "#{k.to_s.camelize}=#{v}"
43
+ end
44
+ end
45
+ end
46
+
47
+ def handle_response(res) # :nodoc:
48
+ if (400..599).include? res.code
49
+ raise Twilio::APIError.new "Error ##{res.parsed_response['code']}: #{res.parsed_response['message']}"
50
+ else
51
+ key = name.demodulize == 'SMS' ? 'sms_messages' : name.demodulize.underscore.pluralize
52
+ res.parsed_response[key].map do |p|
53
+ p['api_version'] = p['api_version'].to_s # api_version parsed as a date by http_party
54
+ new p
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ module Twilio
2
+ class IncomingPhoneNumber
3
+ extend Twilio::Finder
4
+ include Twilio::Resource
5
+ include Twilio::Persistable
6
+ include Twilio::Deletable
7
+
8
+ %w<friendly_name api_version voice_url voice_method voice_fallback_url voice_fallback_method status_callback status_callback_method sms_url sms_method sms_fallback_url sms_fallback_method voice_caller_id_lookup>.each do |meth|
9
+ define_method "#{meth}=" do |arg|
10
+ update_attributes meth => arg
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ module Twilio
2
+ class Notification
3
+ include Twilio::Resource
4
+ include Twilio::Deletable
5
+ extend Twilio::Finder
6
+
7
+ class << self
8
+ private
9
+ def prepare_dates(opts)
10
+ opts.map do |k,v|
11
+ if [:created_before, :created_after].include? k
12
+ k = k.to_s
13
+ # Fancy schmancy-ness to handle Twilio <= URI operator for dates
14
+ comparator = k =~ /before$/ ? '<=' : '>='
15
+ "MessageDate" << comparator << v.to_s
16
+ else
17
+ "#{k.to_s.camelize}=#{v}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ module Twilio
2
+ class OutgoingCallerId
3
+ include Twilio::Resource
4
+ include Twilio::Deletable
5
+ include Twilio::Persistable
6
+ extend Twilio::Finder
7
+
8
+ def friendly_name=(value)
9
+ update_attributes :friendly_name => value
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module Twilio
2
+ class Participant
3
+ include Twilio::Resource
4
+ include Twilio::Deletable
5
+
6
+ alias kick! destroy
7
+
8
+ def mute!
9
+ state_guard do
10
+ update_attributes muted? ? { :muted => false } : { :muted => true }
11
+ end
12
+ end
13
+
14
+ def muted?
15
+ muted
16
+ end
17
+
18
+ private
19
+
20
+ def path
21
+ "/Accounts/#{Twilio::ACCOUNT_SID}/Conferences/#{conference_sid}/Participants/#{call_sid}.json"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Twilio
2
+ module Persistable
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ class << base
7
+ def create(attrs={})
8
+ # All Twilio resources follow a convention, except SMS :(
9
+ resource = name.demodulize
10
+ resource = name == 'Twilio::SMS' ? "SMS/Messages" : resource + 's'
11
+ res = post "/Accounts/#{Twilio::ACCOUNT_SID}/#{resource}.json", :body => Hash[attrs.map { |k,v| [k.to_s.camelize, v]}]
12
+ if (400..599).include? res.code
13
+ raise Twilio::APIError.new "Error ##{res.parsed_response['code']}: #{res.parsed_response['message']}"
14
+ else
15
+ res.parsed_response['api_version'] = res.parsed_response['api_version'].to_s
16
+ new res.parsed_response
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ module Twilio
2
+ class Recording
3
+ include Twilio::Resource
4
+ include Twilio::Deletable
5
+ extend Twilio::Finder
6
+
7
+ def mp3
8
+ API_ENDPOINT + path.gsub(/\.json$/, '.mp3')
9
+ end
10
+ end
11
+ end
@@ -1,28 +1,49 @@
1
1
  require 'active_support/core_ext/string' # Chill! we only use the bits of AS we need!
2
+ require 'active_support/core_ext/hash'
2
3
 
3
4
  module Twilio
4
5
  module Resource
5
- # Convenience for accessing attributes. Attributes can be accessed either using the
6
- # preferred symbol style, e.g. :if_machine or using the Twilio stringified attribute
7
- # style, e.g. 'IfMachine'
8
- # Kind of like ActiveSupport::HashWithIndifferentAccess on crack.
6
+ def initialize(attrs ={}) #:nodoc:
7
+ @attributes = attrs.with_indifferent_access
8
+ end
9
+
9
10
  def [](key)
10
- accessor = key.is_a?(Symbol) ? key.to_s.camelize : key
11
- attributes[accessor]
11
+ attributes[key]
12
+ end
13
+
14
+ def []=(key,value) # :nodoc:
15
+ attributes[key] = value
12
16
  end
13
17
 
14
- def []=(key,value)
15
- accessor = key.is_a?(Symbol) ? key.to_s.camelize : key
16
- attributes[accessor] = value
18
+ def update_attributes(attrs={})
19
+ state_guard do
20
+ handle_response klass.post path, :body => Hash[attrs.map { |k,v| [k.to_s.camelize, v]}]
21
+ end
17
22
  end
18
23
 
19
24
  private
25
+ def klass
26
+ self.class == Module ? self : self.class
27
+ end
28
+
29
+ def state_guard
30
+ if frozen?
31
+ raise RuntimeError, "#{self.class.name.demodulize} has already been destroyed"
32
+ else
33
+ yield
34
+ end
35
+ end
36
+
37
+ def path # :nodoc:
38
+ "/Accounts/#{Twilio::ACCOUNT_SID}/#{klass.name.demodulize.pluralize}/#{self[:sid]}.json"
39
+ end
20
40
 
21
41
  def handle_response(res) # :nodoc:
22
- if res.code.to_s =~ /^(4|5)\d\d/
42
+ if (400..599).include? res.code
23
43
  raise Twilio::APIError.new "Error ##{res.parsed_response['code']}: #{res.parsed_response['message']}"
24
44
  else
25
- @attributes.update Hash[res.parsed_response.map { |k,v| [k.camelize, v] }] # params are camelized in requests, yet underscored in the repsonse. inconsistency FTW!
45
+ res.parsed_response['api_version'] = res.parsed_response['api_version'].to_s
46
+ @attributes.update(res.parsed_response)
26
47
  end
27
48
  end
28
49
 
@@ -31,10 +52,10 @@ module Twilio
31
52
  if meth =~ /\=$/
32
53
  add_attr_writer meth
33
54
  send meth, args.first
34
- elsif meth =~ /^#{meth}\?/i
55
+ elsif meth =~ /^#{meth}\?$/i
35
56
  add_predicate meth
36
57
  send meth
37
- elsif self[id]
58
+ elsif attributes.keys.include? meth
38
59
  add_attr_reader meth
39
60
  send meth
40
61
  else
@@ -44,7 +65,7 @@ module Twilio
44
65
 
45
66
  def add_predicate(attribute)
46
67
  metaclass.class_eval do
47
- define_method(attribute) { self[:status] =~ /^#{attribute.gsub '?', ''}/i ? true : false }
68
+ define_method(attribute) { self['status'] =~ /^#{attribute.gsub '?', ''}/i ? true : false }
48
69
  end
49
70
  end
50
71
 
@@ -56,7 +77,7 @@ module Twilio
56
77
 
57
78
  def add_attr_reader(attribute) #:nodoc
58
79
  metaclass.class_eval do
59
- define_method(attribute) { self[attribute.to_sym] } unless respond_to? attribute
80
+ define_method(attribute) { self[attribute] } unless respond_to? attribute
60
81
  end
61
82
  end
62
83
 
@@ -73,20 +94,6 @@ module Twilio
73
94
  end
74
95
 
75
96
  class << base
76
- if instance_of? Class # Don't want this mixed into singleton objects e.g. Twilio::Account
77
- def find(id)
78
- # All Twilio resources follow a convention, except SMS :(
79
- klass_name = name.demodulize
80
- resource = klass_name == 'SMS' ? "#{klass_name}/Messages" : klass_name.pluralize
81
- res = get "/Accounts/#{Twilio::ACCOUNT_SID}/#{resource}/#{id}.json"
82
- new Hash[res.parsed_response.map { |k,v| [k.camelize, v] }] if (200..299).include? res.code
83
- end
84
-
85
- def create(attrs={})
86
- new(attrs).tap { |c| c.save }
87
- end
88
- end
89
-
90
97
  # decorate http methods with authentication
91
98
  %w<post get put delete>.each do |meth|
92
99
  define_method(meth) do |*args| # splatted args necessary hack since <= 1.8.7 does not support optional block args
@@ -0,0 +1,26 @@
1
+ module Twilio
2
+ module Sandbox
3
+ include Twilio::Resource
4
+ @attributes = {}.with_indifferent_access
5
+
6
+ def attributes
7
+ @attributes.empty? ? reload! : @attributes
8
+ end
9
+
10
+ def reload!
11
+ handle_response get path
12
+ end
13
+
14
+ %w<voice_url voice_method sms_url sms_method>.each do |meth|
15
+ define_method "#{meth}=" do |arg|
16
+ update_attributes meth => arg
17
+ end
18
+ end
19
+
20
+ private
21
+ def path
22
+ "/Accounts/#{Twilio::ACCOUNT_SID}/Sandbox.json"
23
+ end
24
+ extend self
25
+ end
26
+ end
data/lib/twilio/sms.rb CHANGED
@@ -1,14 +1,23 @@
1
1
  module Twilio
2
2
  class SMS
3
3
  include Twilio::Resource
4
+ include Twilio::Persistable
5
+ extend Twilio::Finder
4
6
 
5
- def initialize(attrs ={}) #:nodoc:
6
- @attributes = Hash[attrs.map { |k,v| [k.to_s.camelize, v.to_s] }]
7
- end
8
-
9
- # Sends the SMS message
10
- def save
11
- handle_response self.class.post "/Accounts/#{Twilio::ACCOUNT_SID}/SMS/Messages.json", :body => attributes
7
+ class << self
8
+ private
9
+ def prepare_dates(opts)
10
+ opts.map do |k,v|
11
+ if [:created_before, :created_after, :sent_before, :sent_after].include? k
12
+ k = k.to_s
13
+ # Fancy schmancy-ness to handle Twilio <= URI operator for dates
14
+ comparator = k =~ /before$/ ? '<=' : '>='
15
+ "DateSent" << comparator << v.to_s
16
+ else
17
+ "#{k.to_s.camelize}=#{v}"
18
+ end
19
+ end
20
+ end
12
21
  end
13
22
  end
14
- end
23
+ end
@@ -0,0 +1,6 @@
1
+ module Twilio
2
+ class Transcription
3
+ include Twilio::Resource
4
+ extend Twilio::Finder
5
+ end
6
+ end
data/lib/twilio.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  %w<rubygems active_support cgi yajl yajl/json_gem httparty builder>.each { |lib| require lib }
2
- require File.join(File.dirname(__FILE__), 'twilio', 'resource.rb')
2
+ %w<resource finder persistable deletable>.each { |lib| require File.join(File.dirname(__FILE__), 'twilio', "#{lib}.rb") }
3
3
 
4
4
  module Twilio
5
5
  API_ENDPOINT = 'https://api.twilio.com/2010-04-01'
@@ -9,10 +9,15 @@ module Twilio
9
9
 
10
10
  class << self
11
11
  def const_missing(const_name)
12
- raise Twilio::ConfigurationError.new "Cannot complete request. Please set #{const_name.to_s.downcase} with Twilio::Config.setup first!"
12
+ if [:ACCOUNT_SID, :AUTH_TOKEN].include? const_name
13
+ raise Twilio::ConfigurationError.new "Cannot complete request. Please set #{const_name.to_s.downcase} with Twilio::Config.setup first!"
14
+ else
15
+ super
16
+ end
13
17
  end
14
18
  end
15
19
  end
16
20
 
17
21
  Dir[File.join(File.dirname(__FILE__), 'twilio', '*.rb')].each { |lib| require lib }
18
22
 
23
+ require File.join(File.dirname(__FILE__), 'railtie.rb') if Object.const_defined?(:Rails) && Rails.version =~ /^3/
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twilio-rb
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
4
+ prerelease: true
5
5
  segments:
6
- - 0
7
- - 4
8
- version: "0.4"
6
+ - 1
7
+ - 0beta
8
+ version: 1.0beta
9
9
  platform: ruby
10
10
  authors:
11
11
  - Stevie Graham
@@ -13,7 +13,7 @@ autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
15
 
16
- date: 2010-12-13 00:00:00 +00:00
16
+ date: 2010-12-26 00:00:00 +00:00
17
17
  default_executable:
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
@@ -106,6 +106,21 @@ dependencies:
106
106
  version: 2.2.0
107
107
  type: :development
108
108
  version_requirements: *id006
109
+ - !ruby/object:Gem::Dependency
110
+ name: mocha
111
+ prerelease: false
112
+ requirement: &id007 !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ segments:
118
+ - 0
119
+ - 9
120
+ - 10
121
+ version: 0.9.10
122
+ type: :development
123
+ version_requirements: *id007
109
124
  description: A nice Ruby wrapper for the Twilio REST API
110
125
  email: sjtgraham@mac.com
111
126
  executables: []
@@ -116,11 +131,24 @@ extra_rdoc_files: []
116
131
 
117
132
  files:
118
133
  - README.md
134
+ - lib/railtie.rb
119
135
  - lib/twilio/account.rb
136
+ - lib/twilio/available_phone_number.rb
120
137
  - lib/twilio/call.rb
138
+ - lib/twilio/conference.rb
121
139
  - lib/twilio/config.rb
140
+ - lib/twilio/deletable.rb
141
+ - lib/twilio/finder.rb
142
+ - lib/twilio/incoming_phone_number.rb
143
+ - lib/twilio/notification.rb
144
+ - lib/twilio/outgoing_caller_id.rb
145
+ - lib/twilio/participant.rb
146
+ - lib/twilio/persistable.rb
147
+ - lib/twilio/recording.rb
122
148
  - lib/twilio/resource.rb
149
+ - lib/twilio/sandbox.rb
123
150
  - lib/twilio/sms.rb
151
+ - lib/twilio/transcription.rb
124
152
  - lib/twilio/twiml.rb
125
153
  - lib/twilio.rb
126
154
  has_rdoc: false
@@ -145,11 +173,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
145
173
  required_rubygems_version: !ruby/object:Gem::Requirement
146
174
  none: false
147
175
  requirements:
148
- - - ">="
176
+ - - ">"
149
177
  - !ruby/object:Gem::Version
150
178
  segments:
151
- - 0
152
- version: "0"
179
+ - 1
180
+ - 3
181
+ - 1
182
+ version: 1.3.1
153
183
  requirements: []
154
184
 
155
185
  rubyforge_project: