twilio_contactable 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/controller.rb ADDED
@@ -0,0 +1,78 @@
1
+ module TwilioContactable
2
+ module Controller
3
+
4
+ def self.included(controller)
5
+ controller.instance_eval do
6
+ # the developer should specify which model(s) will be sought
7
+ # when the app receives incoming requests
8
+ protected
9
+ def twilio_contactable(*klasses)
10
+ @@contactable_classes = klasses
11
+ klasses.each do |klass|
12
+ klass.twilio_contactable.controller = self.class.name.underscore.chomp('_controller')
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def receive_sms_message
19
+ unless defined?(@@contactable_classes) && !@@contactable_classes.blank?
20
+ raise RuntimeError, "Please define which model(s) this controller should receive for via the 'twilio_contactable' method"
21
+ end
22
+
23
+ request = params[:request]
24
+ render :text => 'unknown format', :status => 500 and return unless request
25
+ case request['type']
26
+ when 'BLOCK'
27
+ @contactable = find_contactable_by_phone_number(request[:block][:recipient][:id])
28
+ @contactable._TC_sms_blocked = true
29
+ @contactable.save!
30
+ when 'MESSAGE'
31
+ @contactable = find_contactable_by_phone_number(request[:message][:sender][:id])
32
+ if @contactable.respond_to?(:receive_sms)
33
+ @contactable.receive_sms(request[:message][:text])
34
+ else
35
+ warn "An SMS message was received but #{@contactable.class.name.inspect} doesn't have a receive_sms method!"
36
+ end
37
+ end
38
+ render :text => 'OK', :status => 200
39
+ end
40
+
41
+ def start_voice_confirmation
42
+ render :xml => (Twilio::Response.new.tap do |response|
43
+ response.addGather(
44
+ :action => url_for(
45
+ :action => 'receive_voice_confirmation',
46
+ :contactable_type => params[:contactable_type],
47
+ :contactable_id => params[:contactable_id]
48
+ )
49
+ ).tap do |gather|
50
+ gather.addSay "Please type the numbers that appear on your screen, followed by the pound sign"
51
+ end
52
+ end.respond)
53
+ end
54
+
55
+ def receive_voice_confirmation
56
+ @contactable = params[:contactable_type].constantize.find(params[:contactable_id])
57
+ @contactable.voice_confirm_with(params['Digits'])
58
+ end
59
+
60
+ protected
61
+
62
+ def find_contactable_by_phone_number(number)
63
+ [number, number.sub(/^\+/,''), number.sub(/^\+1/,'')].uniq.compact.each do |possible_phone_number|
64
+ @@contactable_classes.each do |klass|
65
+ found = klass.find(
66
+ :first,
67
+ :conditions =>
68
+ { klass.twilio_contactable.formatted_phone_number_column => possible_phone_number }
69
+ )
70
+ return found if found
71
+ end
72
+ end
73
+ nil
74
+ # rescue => error
75
+ # render :text => error.inspect
76
+ end
77
+ end
78
+ end
data/lib/gateway.rb ADDED
@@ -0,0 +1,105 @@
1
+ module TwilioContactable
2
+ module Gateway
3
+ class WebsiteAddressNotConfiguredError < ArgumentError
4
+ def initialize
5
+ super "Please define your `website_address` config parameter "+
6
+ "in the TwilioContactable initializer. "+
7
+ "It should be in the format of: \"http://example.com/\""
8
+
9
+ end
10
+ end
11
+ class ControllerNotConfiguredError < ArgumentError
12
+ def initialize(record)
13
+ super "You're initiating a voice call for a "+
14
+ record.class.name.inspect +
15
+ " but you have not specified this class "+
16
+ "as a possible recipient in a controller."
17
+ end
18
+ end
19
+ class Response
20
+ def initialize(*args)
21
+ @options = args.last
22
+ end
23
+
24
+ def success?
25
+ :success == @options[:status]
26
+ end
27
+ end
28
+ Success = Response.new(:status => :success)
29
+ Error = Response.new(:status => :error)
30
+
31
+ API_VERSION = '2010-04-01'
32
+
33
+ class << self
34
+
35
+ def initiate_voice_call(record, to, from = nil)
36
+
37
+ unless TwilioContactable.configuration.website_address
38
+ raise WebsiteAddressNotConfiguredError.new
39
+ end
40
+ unless controller = record.class.twilio_contactable.controller
41
+ raise ControllerNotConfiguredError.new(record)
42
+ end
43
+
44
+ url = TwilioContactable.configuration.website_address.chomp('/')
45
+ url = "#{url}/#{record.class.twilio_contactable.controller}"
46
+ url = "#{url}/start_voice_confirmation?contactable_type=#{record.class}&contactable_id=#{record.id}"
47
+
48
+ deliver :voice,
49
+ 'To' => to,
50
+ 'From' => from,
51
+ 'Url' => url
52
+ end
53
+
54
+ def deliver_sms(message, to, from = nil)
55
+ deliver :sms,
56
+ 'Message' => message,
57
+ 'To' => to,
58
+ 'From' => from
59
+ end
60
+
61
+ def account
62
+ @account ||= begin
63
+ if TwilioContactable.configuration.client_id.blank? ||
64
+ TwilioContactable.configuration.client_key.blank?
65
+ raise "Add your Twilio account id (as client_id) and token (as client_key) to the TwilioContactable.configure block"
66
+ end
67
+ Twilio::RestAccount.new(
68
+ TwilioContactable.configuration.client_id,
69
+ TwilioContactable.configuration.client_key
70
+ )
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def deliver(type, data = {})
77
+
78
+ data['From'] = TwilioContactable.configuration.default_from_phone_number if data['From'].blank?
79
+ raise "'From' number required for Twilio" unless data['From']
80
+
81
+ service = case type
82
+ when :sms
83
+ 'SMS/Messages'
84
+ when :voice
85
+ 'Calls'
86
+ end
87
+
88
+ response = post service, data
89
+
90
+ Net::HTTPCreated == response.code_type ?
91
+ TwilioContactable::Gateway::Success :
92
+ TwilioContactable::Gateway::Error
93
+ end
94
+
95
+ def post(service, data = {})
96
+ url = "/#{API_VERSION}/Accounts/#{TwilioContactable.configuration.client_id}/#{service}"
97
+ puts "putting data: #{data.inspect}"
98
+ puts "to url: #{url}"
99
+ account.request url,
100
+ "POST",
101
+ data
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,40 @@
1
+ module TwilioContactable
2
+ class << self
3
+ def numerize(numberish)
4
+ numberish.to_s.scan(/\d+/).join
5
+ end
6
+
7
+ def internationalize(given_number)
8
+ number = numerize(given_number)
9
+ case number.size
10
+ when 10
11
+ "+1#{number}"
12
+ when 11
13
+ "+#{number}"
14
+ when 12
15
+ number =~ /\+\d(11)/ ? number : nil
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def confirmation_message(confirmation_code)
22
+ "Code: #{confirmation_code} Enter code on web to verify phone. Msg&data rates may apply. Freq set by u. T&C & support on web site. Txt HELP for help"
23
+ end
24
+
25
+ def generate_confirmation_code
26
+ chars = (0..9).to_a + ('A'..'Z').to_a
27
+ (0...6).collect { chars[Kernel.rand(chars.length)] }.join
28
+ end
29
+ end
30
+ end
31
+
32
+ gem 'twiliolib'
33
+ require 'twiliolib'
34
+
35
+ require File.expand_path(File.join(File.dirname(__FILE__), 'configuration'))
36
+ require File.expand_path(File.join(File.dirname(__FILE__), 'gateway'))
37
+ require File.expand_path(File.join(File.dirname(__FILE__), 'contactable'))
38
+ require File.expand_path(File.join(File.dirname(__FILE__), 'controller'))
39
+
40
+ ActiveRecord::Base.send :include, TwilioContactable::Contactable
data/test/.gitignore ADDED
@@ -0,0 +1 @@
1
+ debug.log
data/test/database.yml ADDED
@@ -0,0 +1,18 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :database: plugin.sqlite.db
4
+ sqlite3:
5
+ :adapter: sqlite3
6
+ :database: ":memory:"
7
+ postgresql:
8
+ :adapter: postgresql
9
+ :username: postgres
10
+ :password: postgres
11
+ :database: plugin_test
12
+ :min_messages: ERROR
13
+ mysql:
14
+ :adapter: mysql
15
+ :host: localhost
16
+ :username: rails
17
+ :password:
18
+ :database: plugin_test
@@ -0,0 +1,63 @@
1
+ require 'rubygems'
2
+ gem 'test-unit'
3
+ require 'test/unit'
4
+ require 'mocha'
5
+ gem 'activesupport', '< 3.0.0'
6
+ require 'active_support'
7
+ require 'active_support/test_case'
8
+ gem 'activerecord', '< 3.0.0'
9
+ require 'active_record'
10
+ gem 'actionpack', '< 3.0.0'
11
+ require 'actionpack'
12
+ require 'action_controller'
13
+ require 'shoulda'
14
+ require 'shoulda/action_controller'
15
+ require 'shoulda/action_controller/macros'
16
+ require 'shoulda/action_controller/matchers'
17
+ require File.join(File.dirname(__FILE__), "..", 'lib', 'twilio_contactable')
18
+
19
+ # default test configuration
20
+ TwilioContactable.configure do |config|
21
+ config.client_id = '1'
22
+ config.client_key = 'ABCDEF'
23
+ config.website_address = 'http://example.com'
24
+ config.default_from_phone_number = '1 (206) 867-5309'
25
+ end
26
+
27
+ TwilioContactable.mode = :test
28
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
29
+ require 'logger'
30
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
31
+ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
32
+
33
+ ActiveRecord::Schema.define(:version => 1) do
34
+ create_table :users do |t|
35
+ t.column :phone_number, :string
36
+ t.column :formatted_phone_number, :string
37
+ t.column :sms_blocked, :boolean
38
+ t.column :sms_confirmation_code, :string
39
+ t.column :sms_confirmation_attempted, :datetime
40
+ t.column :sms_confirmed_phone_number, :string
41
+ t.column :voice_blocked, :boolean
42
+ t.column :voice_confirmation_code, :string
43
+ t.column :voice_confirmation_attempted, :datetime
44
+ t.column :voice_confirmed_phone_number, :string
45
+
46
+ t.column :custom_column, :string
47
+
48
+ t.column :type, :string
49
+ end
50
+ end
51
+
52
+ class User < ActiveRecord::Base
53
+ include TwilioContactable::Contactable
54
+ end
55
+
56
+ # kill all network access
57
+ module TwilioContactable
58
+ module Gateway
59
+ def post
60
+ raise "You forgot to stub out your net requests!"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,308 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+
3
+ class TwilioContactableContactableTest < ActiveSupport::TestCase
4
+
5
+ Success = TwilioContactable::Gateway::Response.new(:status => :success)
6
+ Error = TwilioContactable::Gateway::Response.new(:status => :error)
7
+
8
+ context "configuration" do
9
+ TwilioContactable::Contactable::Attributes.each do |attr|
10
+ context "attribute: #{attr}" do
11
+ should "begin with appropriate default for #{attr}_column and allow overwriting" do
12
+ klass = Class.new
13
+ klass.send :include, TwilioContactable::Contactable
14
+ assert_equal attr, klass.twilio_contactable.send("#{attr}_column")
15
+ klass.twilio_contactable do |config|
16
+ config.send("#{attr}_column=", :custom_column)
17
+ end
18
+ assert_equal :custom_column, klass.twilio_contactable.send("#{attr}_column")
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ context "contactable instance" do
25
+ setup {
26
+ User.delete_all
27
+ @user = User.create! :phone_number => '(555) 123-4567'
28
+ }
29
+
30
+ should "normalize phone number" do
31
+ assert_equal '+15551234567', @user.formatted_phone_number
32
+ end
33
+ context "when phone number is blank" do
34
+ setup { @user._TC_phone_number = nil}
35
+ context "confirming phone number" do
36
+ setup { @user.send_sms_confirmation! }
37
+ should_not_change "any attributes" do
38
+ @user.attributes.inspect
39
+ end
40
+ end
41
+ context "sending message" do
42
+ setup {
43
+ TwilioContactable::Gateway.stubs(:perform).returns(Success)
44
+ @worked = @user.send_sms!('message')
45
+ }
46
+ should "not work" do assert !@worked end
47
+ should_not_change "any attributes" do
48
+ @user.attributes.inspect
49
+ end
50
+ end
51
+ end
52
+
53
+ context "when phone number exists" do
54
+ setup { @user._TC_phone_number = "206-555-5555"}
55
+ context "confirming phone number" do
56
+ context "via sms" do
57
+ setup {
58
+ TwilioContactable::Gateway.stubs(:deliver).returns(Success)
59
+ @worked = @user.send_sms_confirmation!
60
+ }
61
+ should "work" do assert @worked end
62
+ should "save confirmation number in proper attribute" do
63
+ assert @user._TC_sms_confirmation_code
64
+ end
65
+ should "set confirmation attempted time" do
66
+ assert @user._TC_sms_confirmation_attempted > 3.minutes.ago
67
+ end
68
+ should_change "stored code" do
69
+ @user._TC_sms_confirmation_code
70
+ end
71
+ should "not have number confirmed yet" do
72
+ assert !@user.sms_confirmed?
73
+ end
74
+ context "calling sms_confirm_with(right_code)" do
75
+ setup { @user.sms_confirm_with(@user._TC_sms_confirmation_code) }
76
+ should "work" do
77
+ assert @worked
78
+ end
79
+ should "save the phone number into the confirmed attribute" do
80
+ assert_equal @user._TC_phone_number,
81
+ @user._TC_sms_confirmed_phone_number,
82
+ @user.reload.inspect
83
+ end
84
+ should_change "confirmed phone number attribute" do
85
+ @user._TC_sms_confirmed_phone_number
86
+ end
87
+ context "and then attempting to confirm another number" do
88
+ setup {
89
+ @user._TC_phone_number = "206-555-8990"
90
+ TwilioContactable::Gateway.stubs(:deliver).returns(Success).once
91
+ @user.send_sms_confirmation!
92
+ }
93
+ should "eliminate the previous confirmed phone number" do
94
+ assert @user._TC_sms_confirmed_phone_number.blank?
95
+ end
96
+ should "un-confirm the record" do
97
+ assert !@user.sms_confirmed?
98
+ end
99
+ end
100
+ end
101
+ context "calling sms_confirm_with(right code, wrong case)" do
102
+ setup {
103
+ @downcased_code = @user._TC_sms_confirmation_code.downcase
104
+ @worked = @user.sms_confirm_with(@downcased_code)
105
+ }
106
+ should "have good test data" do
107
+ assert_not_equal @downcased_code,
108
+ @user._TC_sms_confirmation_code
109
+ end
110
+ should "work" do
111
+ assert @worked
112
+ end
113
+ should "save the phone number into the confirmed attribute" do
114
+ assert_equal @user._TC_sms_confirmed_phone_number,
115
+ @user._TC_phone_number
116
+ end
117
+ end
118
+ context "calling sms_confirm_with(wrong_code)" do
119
+ setup { @worked = @user.sms_confirm_with('wrong_code') }
120
+ should "not work" do
121
+ assert !@worked
122
+ end
123
+ should "not save the phone number into the confirmed attribute" do
124
+ assert_not_equal @user._TC_sms_confirmed_phone_number,
125
+ @user._TC_phone_number
126
+ end
127
+ should_not_change "confirmed phone number attribute" do
128
+ @user.reload._TC_sms_confirmed_phone_number
129
+ end
130
+ end
131
+ end
132
+ context "confirming phone number with a custom short code" do
133
+ context "with expectations" do
134
+ setup {
135
+ message = "long message blah blah MYCODE blah"
136
+ TwilioContactable.expects(:generate_confirmation_code).returns('MYCODE').once
137
+ TwilioContactable.expects(:confirmation_message).returns(message).once
138
+ TwilioContactable::Gateway.expects(:deliver).with(message, @user._TC_phone_number).once
139
+ @user.send_sms_confirmation!
140
+ }
141
+ end
142
+ context "(normal)" do
143
+ setup {
144
+ TwilioContactable::Gateway.stubs(:deliver).returns(Success)
145
+ @worked = @user.send_sms_confirmation!
146
+ }
147
+ should "work" do
148
+ assert @worked
149
+ end
150
+ end
151
+ end
152
+ context "confirming phone number when the confirmation fails for some reason" do
153
+ setup {
154
+ TwilioContactable::Gateway.stubs(:deliver).returns(Error)
155
+ @worked = @user.send_sms_confirmation!
156
+ }
157
+ should "not work" do assert !@worked end
158
+ should "not save confirmation number" do
159
+ assert @user._TC_sms_confirmation_code.blank?
160
+ end
161
+ should_not_change "stored code" do
162
+ @user._TC_sms_confirmation_code
163
+ end
164
+ end
165
+ context "via voice" do
166
+ setup {
167
+ TwilioContactable::Gateway.stubs(:deliver).returns(Success)
168
+ # To start any voice call we'll need to first initialize
169
+ # this model in a controller
170
+ class TestController < ActionController::Base
171
+ include TwilioContactable::Controller
172
+ twilio_contactable User
173
+ self
174
+ end
175
+ @worked = @user.send_voice_confirmation!
176
+ }
177
+ should "work" do assert @worked end
178
+ should "save confirmation number in proper attribute" do
179
+ assert @user._TC_voice_confirmation_code
180
+ end
181
+ should "set confirmation attempted time" do
182
+ assert @user._TC_voice_confirmation_attempted > 3.minutes.ago
183
+ end
184
+ should_change "stored code" do
185
+ @user._TC_voice_confirmation_code
186
+ end
187
+ should "not have number confirmed yet" do
188
+ assert !@user.voice_confirmed?
189
+ end
190
+ context "calling voice_confirm_with(right_code)" do
191
+ setup { @user.voice_confirm_with(@user._TC_voice_confirmation_code) }
192
+ should "work" do
193
+ assert @worked
194
+ end
195
+ should "save the phone number into the confirmed attribute" do
196
+ assert_equal @user._TC_phone_number,
197
+ @user._TC_voice_confirmed_phone_number,
198
+ @user.reload.inspect
199
+ end
200
+ should_change "confirmed phone number attribute" do
201
+ @user._TC_voice_confirmed_phone_number
202
+ end
203
+ context "and then attempting to confirm another number" do
204
+ setup {
205
+ @user._TC_phone_number = "206-555-8990"
206
+ TwilioContactable::Gateway.stubs(:deliver).returns(Success).once
207
+ @user.send_voice_confirmation!
208
+ }
209
+ should "eliminate the previous confirmed phone number" do
210
+ assert @user._TC_voice_confirmed_phone_number.blank?
211
+ end
212
+ should "un-confirm the record" do
213
+ assert !@user.voice_confirmed?
214
+ end
215
+ end
216
+ end
217
+ context "calling voice_confirm_with(right code, wrong case)" do
218
+ setup {
219
+ @downcased_code = @user._TC_voice_confirmation_code.downcase
220
+ @worked = @user.voice_confirm_with(@downcased_code)
221
+ }
222
+ should "have good test data" do
223
+ assert_not_equal @downcased_code,
224
+ @user._TC_voice_confirmation_code
225
+ end
226
+ should "work" do
227
+ assert @worked
228
+ end
229
+ should "save the phone number into the confirmed attribute" do
230
+ assert_equal @user._TC_voice_confirmed_phone_number,
231
+ @user._TC_phone_number
232
+ end
233
+ end
234
+ context "calling voice_confirm_with(wrong_code)" do
235
+ setup { @worked = @user.voice_confirm_with('wrong_code') }
236
+ should "not work" do
237
+ assert !@worked
238
+ end
239
+ should "not save the phone number into the confirmed attribute" do
240
+ assert_not_equal @user._TC_voice_confirmed_phone_number,
241
+ @user._TC_phone_number
242
+ end
243
+ should_not_change "confirmed phone number attribute" do
244
+ @user.reload._TC_voice_confirmed_phone_number
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ context "when the number is not confirmed" do
252
+ context "sending a message" do
253
+ setup {
254
+ TwilioContactable::Gateway.stubs(:deliver).returns(Success)
255
+ @result = @user.send_sms!('message')
256
+ }
257
+ should "send send no messages" do
258
+ assert_equal false, @result
259
+ end
260
+ end
261
+ end
262
+ context "when the number is blocked" do
263
+ setup {
264
+ @user._TC_sms_blocked = true
265
+ @user.save!
266
+ }
267
+ context "sending a message" do
268
+ setup { @result = @user.send_sms!('message') }
269
+ should "send nothing" do
270
+ assert_equal false, @result
271
+ end
272
+ end
273
+ end
274
+ context "when the number is confirmed" do
275
+ setup {
276
+ TwilioContactable::Gateway.stubs(:deliver).returns(Success)
277
+ @user.stubs(:sms_confirmed?).returns(true)
278
+ }
279
+ context "sending a message" do
280
+ setup { @result = @user.send_sms!('message') }
281
+ should "send send exactly one message" do
282
+ assert_equal [7], @result
283
+ end
284
+ end
285
+ context "sending a blank message" do
286
+ setup { @result = @user.send_sms!('') }
287
+ should "send send zero messages" do
288
+ assert_equal false, @result
289
+ end
290
+ end
291
+ context "sending a huge message" do
292
+ context "without the allow_multiple flag" do
293
+ should "raise an error" do
294
+ assert_raises ArgumentError do
295
+ @user.send_sms!("A"*200)
296
+ end
297
+ end
298
+ end
299
+ context "with the allow_multiple flag" do
300
+ setup { @result = @user.send_sms!("A"*200, true) }
301
+ should "send multiple messages" do
302
+ assert_equal [160, 40], @result
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end