twilio_contactable 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.markdown +144 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/init.rb +3 -0
- data/lib/configuration.rb +41 -0
- data/lib/contactable.rb +204 -0
- data/lib/controller.rb +78 -0
- data/lib/gateway.rb +105 -0
- data/lib/twilio_contactable.rb +40 -0
- data/test/.gitignore +1 -0
- data/test/database.yml +18 -0
- data/test/test_helper.rb +63 -0
- data/test/twilio_contactable_contactable_test.rb +308 -0
- data/test/twilio_contactable_controller_test.rb +97 -0
- data/test/twilio_module_test.rb +56 -0
- data/twilio_contactable.gemspec +68 -0
- metadata +118 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
Twilo Contactable
|
2
|
+
=====
|
3
|
+
|
4
|
+
Twilo makes voice and SMS interactions easy. But if you want to be able to seamlessly validate your user's phone numbers for
|
5
|
+
both voice and text there's a lot of work you'll have to do in your Rails app. Unless you use this gem.
|
6
|
+
|
7
|
+
Why bother?
|
8
|
+
=====
|
9
|
+
|
10
|
+
Unless you're programming Ruby like it's PHP you don't enjoy passing strings around and writing all procedural code. This gem lets you
|
11
|
+
ask for a phone number from your users, confirm their ownership of it via SMS or Voice or both, and keep track of whether the number is
|
12
|
+
still validated when they edit it.
|
13
|
+
|
14
|
+
|
15
|
+
Setting Up Your Model
|
16
|
+
=====
|
17
|
+
|
18
|
+
Include Twilio::Contactable into your User class or whatever you're using to represent an entity with a phone number.
|
19
|
+
|
20
|
+
class User < ActiveRecord::Base
|
21
|
+
twilio_contactable
|
22
|
+
end
|
23
|
+
|
24
|
+
You can also specify which attributes you'd like to use instead of the defaults
|
25
|
+
|
26
|
+
class User < ActiveRecord::Base
|
27
|
+
twilio_contactable do |config|
|
28
|
+
config.phone_number_column :mobile_number
|
29
|
+
config.formatted_phone_number_column :formatted_mobile_number
|
30
|
+
config.sms_blocked_column :should_we_not_txt_this_user
|
31
|
+
config.sms_confirmation_code_column :the_sms_confirmation_code
|
32
|
+
config.sms_confirmation_attempted_column :when_was_the_sms_confirmation_attempted
|
33
|
+
config.sms_confirmed_phone_number_column :the_mobile_number_thats_been_confirmed_for_sms
|
34
|
+
config.voice_blocked_column :should_we_not_call_this_user
|
35
|
+
config.voice_confirmation_code_column :the_voice_confirmation_code
|
36
|
+
config.voice_confirmation_attempted_column :when_was_the_voice_confirmation_attempted
|
37
|
+
config.voice_confirmed_phone_number_column :the_mobile_number_thats_been_confirmed_for_voice
|
38
|
+
|
39
|
+
# Defaults to the name on the left (minus the '_column' at the end)
|
40
|
+
# e.g., the sms_blocked_column is 'sms_blocked'
|
41
|
+
#
|
42
|
+
# You don't need all those columns, omit any that you're sure you won't want.
|
43
|
+
end
|
44
|
+
|
45
|
+
Turning the thing on
|
46
|
+
---
|
47
|
+
|
48
|
+
Because it can be expensive to send TXTs or make calls accidentally, it's required that you manually configure TwilioContactable in your app. Put this line in config/environments/production.rb or anything that loads _only_ in your production environment:
|
49
|
+
|
50
|
+
TwilioContactable.mode = :live
|
51
|
+
|
52
|
+
Skipping this step (or adding any other value) will prevent TXTs from actually being sent.
|
53
|
+
|
54
|
+
You'll also want to configure your setup with your client_id and client_key. Put this in the same file as above or in a separate initializer if you wish:
|
55
|
+
|
56
|
+
TwilioContactable.configure do |config|
|
57
|
+
# these three are required:
|
58
|
+
# (replace them with your actual account info)
|
59
|
+
config.client_id = 12345
|
60
|
+
config.client_key = 'ABC123'
|
61
|
+
config.website_address = 'http://myrubyapp.com' # <- Twilio.com needs to be able to find this
|
62
|
+
|
63
|
+
# the rest are optional:
|
64
|
+
config.short_code = 00001 # if you have a custom short code
|
65
|
+
config.proxy_address = 'my.proxy.com'
|
66
|
+
config.proxy_port = '80'
|
67
|
+
config.proxy_username = 'user'
|
68
|
+
config.proxy_password = 'password'
|
69
|
+
end
|
70
|
+
|
71
|
+
Phone number formatting
|
72
|
+
---
|
73
|
+
|
74
|
+
Whatever is stored in the phone_number_column will be subject to normalized formatting:
|
75
|
+
|
76
|
+
user = User.create :phone_number => '(206) 555-1234'
|
77
|
+
user.phone_number # => (206) 555-1234
|
78
|
+
user.formatted_phone_number # => 12065551234 (defaults to US country code)
|
79
|
+
|
80
|
+
If you want to preserve the format of the number exactly as the user entered it you'll want
|
81
|
+
to save that in a different attribute.
|
82
|
+
|
83
|
+
|
84
|
+
Confirming Phone Number And Sending Messages
|
85
|
+
====
|
86
|
+
|
87
|
+
When your users first hand you their number it will be unconfirmed:
|
88
|
+
|
89
|
+
@user = User.create(:phone_number => '555-222-3333')
|
90
|
+
@user.send_sms_confirmation! # fires off a TXT to the user with a generated confirmation code
|
91
|
+
@user.sms_confirmed? # => false, because we've only started the process
|
92
|
+
|
93
|
+
then ask the user for the confirmation code off their phone and pass it in to sms_confirm_with:
|
94
|
+
|
95
|
+
@user.sms_confirm_with('123XYZ')
|
96
|
+
|
97
|
+
If the code is right then the user's current phone number will be automatically marked as confirmed. You can check this at any time with:
|
98
|
+
|
99
|
+
@user.sms_confirmed? # => true
|
100
|
+
@user.send_sms!("Hi! This is a text message.")
|
101
|
+
|
102
|
+
If the code is wrong then the user's current phone number will stay unconfirmed.
|
103
|
+
|
104
|
+
@user.sms_confirmed? # => false
|
105
|
+
@user.send_sms!("Hi! This is a text message.") # sends nothing
|
106
|
+
|
107
|
+
|
108
|
+
Receiving TXTs and Voice calls
|
109
|
+
====
|
110
|
+
|
111
|
+
You can also receive data posted to you from Twilio. This is how you'll receive messages, txts and notices that users have been blocked.
|
112
|
+
All you need is to create a bare controller and include TwilioContactable::Controller into it. Then specify which Ruby class you're using as a contactable user model (likely User)
|
113
|
+
|
114
|
+
|
115
|
+
class SMSController < ApplicationController
|
116
|
+
include TwilioContactable::Controller
|
117
|
+
|
118
|
+
sms_contactable User # or whichever class you included TwilioContactable::Contactable into
|
119
|
+
end
|
120
|
+
|
121
|
+
And hook this up in your routes.rb file like so:
|
122
|
+
|
123
|
+
ActionController::Routing::Routes.draw do |map|
|
124
|
+
map.route 'twilio', :controller => 'twilio_contactable', :action => :index
|
125
|
+
end
|
126
|
+
|
127
|
+
Now just tell Twilio to POST messages and block notices to you at:
|
128
|
+
|
129
|
+
http://myrubyapp.com/twilio
|
130
|
+
|
131
|
+
Now if your users reply to an SMS with 'STOP' or 'BLOCK' your database will be automatically updated to reflect this.
|
132
|
+
|
133
|
+
Incoming messages from a user will automatically be sent to that user's record:
|
134
|
+
|
135
|
+
# If "I love you!" is sent to you from a user with
|
136
|
+
# the phone number "555-111-9999"
|
137
|
+
# then the following will be executed:
|
138
|
+
User.find_by_phone_number('5551119999').receive_sms("I love you!")
|
139
|
+
|
140
|
+
It's up to you to implement the 'receive_sms' method on User.
|
141
|
+
|
142
|
+
That's it. Patches welcome, forks celebrated.
|
143
|
+
|
144
|
+
Copyright (c) 2010 [Jack Danger Canty](http://jåck.com/), released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "twilio_contactable"
|
8
|
+
gem.summary = %Q{Help authorize the users of your Rails apps to confirm and use their phone numbers}
|
9
|
+
gem.description = %Q{Does all the hard work with letting you confirm your user's phone numbers for Voice or TXT over the Twilio API}
|
10
|
+
gem.email = "gitcommit@6brand.com"
|
11
|
+
gem.homepage = "http://github.com/JackDanger/twilio_contactable"
|
12
|
+
gem.authors = ["Jack Danger Canty"]
|
13
|
+
gem.add_dependency "twiliolib", ">= 2.0.5"
|
14
|
+
gem.add_development_dependency "shoulda", ">= 0"
|
15
|
+
gem.add_development_dependency "mocha", ">= 0"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'rake/testtask'
|
24
|
+
desc "Test Twilio Contactable"
|
25
|
+
Rake::TestTask.new(:test) do |test|
|
26
|
+
test.libs << 'lib' << 'test'
|
27
|
+
test.pattern = 'test/**/*_test.rb'
|
28
|
+
test.verbose = true
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
require 'rcov/rcovtask'
|
33
|
+
Rcov::RcovTask.new do |test|
|
34
|
+
test.libs << 'test'
|
35
|
+
test.pattern = 'test/**/test_*.rb'
|
36
|
+
test.verbose = true
|
37
|
+
end
|
38
|
+
rescue LoadError
|
39
|
+
task :rcov do
|
40
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
task :test => :check_dependencies
|
45
|
+
|
46
|
+
task :default => :test
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.7.1
|
data/init.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module TwilioContactable
|
2
|
+
class << self
|
3
|
+
def mode
|
4
|
+
@@mode ||= :test
|
5
|
+
end
|
6
|
+
|
7
|
+
def mode=(new_mode)
|
8
|
+
@@mode = new_mode
|
9
|
+
end
|
10
|
+
|
11
|
+
def configured?
|
12
|
+
return false unless configuration
|
13
|
+
configuration.client_id && configuration.client_key
|
14
|
+
end
|
15
|
+
|
16
|
+
def configuration
|
17
|
+
@@configuration ||= Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(&block)
|
21
|
+
@@configuration = Configuration.new(&block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Configuration
|
26
|
+
|
27
|
+
attr_accessor :client_id
|
28
|
+
attr_accessor :client_key
|
29
|
+
attr_accessor :short_code
|
30
|
+
attr_accessor :website_address
|
31
|
+
attr_accessor :default_from_phone_number
|
32
|
+
attr_accessor :proxy_address
|
33
|
+
attr_accessor :proxy_port
|
34
|
+
attr_accessor :proxy_username
|
35
|
+
attr_accessor :proxy_password
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
yield self
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/contactable.rb
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
module TwilioContactable
|
2
|
+
module Contactable
|
3
|
+
|
4
|
+
Attributes = [
|
5
|
+
:phone_number,
|
6
|
+
:formatted_phone_number,
|
7
|
+
:sms_blocked,
|
8
|
+
:sms_confirmation_code,
|
9
|
+
:sms_confirmation_attempted,
|
10
|
+
:sms_confirmed_phone_number,
|
11
|
+
:voice_blocked,
|
12
|
+
:voice_confirmation_code,
|
13
|
+
:voice_confirmation_attempted,
|
14
|
+
:voice_confirmed_phone_number
|
15
|
+
]
|
16
|
+
|
17
|
+
class Configuration
|
18
|
+
Attributes.each do |attr|
|
19
|
+
attr_accessor "#{attr}_column"
|
20
|
+
end
|
21
|
+
# the following is set when a controller includes TwilioContactable
|
22
|
+
# and calls twilio_contactable with this model as an argument
|
23
|
+
attr_accessor :controller
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
|
27
|
+
yield self if block_given?
|
28
|
+
|
29
|
+
Attributes.each do |attr|
|
30
|
+
# set the defaults if the user hasn't specified anything
|
31
|
+
if send("#{attr}_column").blank?
|
32
|
+
send("#{attr}_column=", attr)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.included(model)
|
39
|
+
|
40
|
+
# set up the configuration, available within the class object
|
41
|
+
# via this same 'twilio_contactable' method
|
42
|
+
model.instance_eval do
|
43
|
+
def twilio_contactable(&block)
|
44
|
+
@@twilio_contactable = Configuration.new(&block) if block
|
45
|
+
@@twilio_contactable ||= Configuration.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# normalize the phone number before it's saved in the database
|
50
|
+
# (only for model classes using callbacks a la ActiveModel,
|
51
|
+
# other folks will have to do this by hand)
|
52
|
+
if model.respond_to?(:before_save)
|
53
|
+
model.before_save :format_phone_number
|
54
|
+
model.class_eval do
|
55
|
+
def format_phone_number
|
56
|
+
self._TC_formatted_phone_number =
|
57
|
+
TwilioContactable.internationalize(_TC_phone_number)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Set up a bridge to access the data for a specific instance
|
64
|
+
# by referring to the column values in the configuration.
|
65
|
+
def twilio_contactable
|
66
|
+
self.class.twilio_contactable
|
67
|
+
end
|
68
|
+
Attributes.each do |attr|
|
69
|
+
eval %Q{
|
70
|
+
def _TC_#{attr}
|
71
|
+
read_attribute self.class.twilio_contactable.#{attr}_column
|
72
|
+
end
|
73
|
+
def _TC_#{attr}=(value)
|
74
|
+
write_attribute self.class.twilio_contactable.#{attr}_column, value
|
75
|
+
end
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
# Sends an SMS validation request through the gateway
|
82
|
+
def send_sms_confirmation!
|
83
|
+
return false if _TC_sms_blocked
|
84
|
+
return true if sms_confirmed?
|
85
|
+
return false if _TC_phone_number.blank?
|
86
|
+
|
87
|
+
confirmation_code = TwilioContactable.generate_confirmation_code
|
88
|
+
|
89
|
+
# Use this class' confirmation_message method if it
|
90
|
+
# exists, otherwise use the generic message
|
91
|
+
message = (self.class.respond_to?(:confirmation_message) ?
|
92
|
+
self.class :
|
93
|
+
TwilioContactable).confirmation_message(confirmation_code)
|
94
|
+
|
95
|
+
if message.to_s.size > 160
|
96
|
+
raise ArgumentError, "SMS Confirmation Message is too long. Limit it to 160 characters of unescaped text."
|
97
|
+
end
|
98
|
+
|
99
|
+
response = TwilioContactable::Gateway.deliver(message, _TC_phone_number)
|
100
|
+
|
101
|
+
if response.success?
|
102
|
+
update_twilio_contactable_sms_confirmation confirmation_code
|
103
|
+
else
|
104
|
+
false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Begins a phone call to the user where they'll need to type
|
109
|
+
# their confirmation code
|
110
|
+
def send_voice_confirmation!
|
111
|
+
return false if _TC_voice_blocked
|
112
|
+
return true if voice_confirmed?
|
113
|
+
return false if _TC_phone_number.blank?
|
114
|
+
|
115
|
+
confirmation_code = TwilioContactable.generate_confirmation_code
|
116
|
+
|
117
|
+
response = TwilioContactable::Gateway.initiate_voice_call(self, _TC_phone_number)
|
118
|
+
|
119
|
+
if response.success?
|
120
|
+
update_twilio_contactable_voice_confirmation confirmation_code
|
121
|
+
else
|
122
|
+
false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Compares user-provided code with the stored confirmation
|
127
|
+
# code. If they match then the current phone number is set
|
128
|
+
# as confirmed by the user.
|
129
|
+
def sms_confirm_with(code)
|
130
|
+
if _TC_sms_confirmation_code.to_s.downcase == code.downcase
|
131
|
+
# save the phone number into the 'confirmed phone number' attribute
|
132
|
+
self._TC_sms_confirmed_phone_number = _TC_phone_number
|
133
|
+
save
|
134
|
+
else
|
135
|
+
false
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns true if the current phone number has been confirmed by
|
140
|
+
# the user for recieving TXT messages.
|
141
|
+
def sms_confirmed?
|
142
|
+
return false if _TC_sms_confirmed_phone_number.blank?
|
143
|
+
self._TC_sms_confirmed_phone_number == _TC_phone_number
|
144
|
+
end
|
145
|
+
|
146
|
+
# Compares user-provided code with the stored confirmation
|
147
|
+
# code. If they match then the current phone number is set
|
148
|
+
# as confirmed by the user.
|
149
|
+
def voice_confirm_with(code)
|
150
|
+
if _TC_voice_confirmation_code.to_s.downcase == code.downcase
|
151
|
+
# save the phone number into the 'confirmed phone number' attribute
|
152
|
+
self._TC_voice_confirmed_phone_number = _TC_phone_number
|
153
|
+
save
|
154
|
+
else
|
155
|
+
false
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns true if the current phone number has been confirmed by
|
160
|
+
# the user by receiving a phone call
|
161
|
+
def voice_confirmed?
|
162
|
+
return false if _TC_voice_confirmed_phone_number.blank?
|
163
|
+
self._TC_voice_confirmed_phone_number == _TC_phone_number
|
164
|
+
end
|
165
|
+
|
166
|
+
# Sends one or more TXT messages to the contactable record's
|
167
|
+
# mobile number (if the number has been confirmed).
|
168
|
+
# Any messages longer than 160 characters will need to be accompanied
|
169
|
+
# by a second argument <tt>true</tt> to clarify that sending
|
170
|
+
# multiple messages is intentional.
|
171
|
+
def send_sms!(msg, allow_multiple = false)
|
172
|
+
if msg.to_s.size > 160 && !allow_multiple
|
173
|
+
raise ArgumentError, "SMS Message is too long. Either specify that you want multiple messages or shorten the string."
|
174
|
+
end
|
175
|
+
return false if msg.to_s.strip.blank? || _TC_sms_blocked
|
176
|
+
return false unless sms_confirmed?
|
177
|
+
|
178
|
+
# split into pieces that fit as individual messages.
|
179
|
+
msg.to_s.scan(/.{1,160}/m).map do |text|
|
180
|
+
if TwilioContactable::Gateway.deliver_sms(text, _TC_phone_number).success?
|
181
|
+
text.size
|
182
|
+
else
|
183
|
+
false
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
protected
|
189
|
+
|
190
|
+
def update_twilio_contactable_sms_confirmation(new_code)
|
191
|
+
self._TC_sms_confirmation_code = new_code
|
192
|
+
self._TC_sms_confirmation_attempted = Time.now.utc
|
193
|
+
self._TC_sms_confirmed_phone_number = nil
|
194
|
+
save
|
195
|
+
end
|
196
|
+
|
197
|
+
def update_twilio_contactable_voice_confirmation(new_code)
|
198
|
+
self._TC_voice_confirmation_code = new_code
|
199
|
+
self._TC_voice_confirmation_attempted = Time.now.utc
|
200
|
+
self._TC_voice_confirmed_phone_number = nil
|
201
|
+
save
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|