twilio_contactable 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|