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 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,3 @@
1
+ # Include hook code here
2
+
3
+ require 'twilio_contactable'
@@ -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
@@ -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