txter 2.0.2
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 +125 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/init.rb +3 -0
- data/lib/4info_templates/confirm.haml +6 -0
- data/lib/4info_templates/deliver.haml +11 -0
- data/lib/4info_templates/unblock.haml +6 -0
- data/lib/configuration.rb +41 -0
- data/lib/contactable.rb +150 -0
- data/lib/controller.rb +62 -0
- data/lib/gateway.rb +45 -0
- data/lib/gateway_4info.rb +114 -0
- data/lib/gateway_twilio.rb +45 -0
- data/lib/txter.rb +50 -0
- data/test/.gitignore +1 -0
- data/test/database.yml +18 -0
- data/test/gateway_4info_test.rb +75 -0
- data/test/gateway_twilio_test.rb +15 -0
- data/test/test_helper.rb +46 -0
- data/test/txter_contactable_test.rb +270 -0
- data/test/txter_controller_test.rb +76 -0
- data/test/txter_module_test.rb +56 -0
- data/txter.gemspec +74 -0
- metadata +102 -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,125 @@
|
|
1
|
+
4info
|
2
|
+
=====
|
3
|
+
|
4
|
+
Connect to the 4info SMS gateway
|
5
|
+
|
6
|
+
If you're using 4info.com as your SMS gateway this gem will give you a painless API for both sending and receiving messages.
|
7
|
+
|
8
|
+
Setting Up Your Model
|
9
|
+
=====
|
10
|
+
|
11
|
+
Include Txter::Contactable into your User class or whatever you're using to represent an entity with a phone number.
|
12
|
+
|
13
|
+
class User < ActiveRecord::Base
|
14
|
+
include Txter::Contactable
|
15
|
+
end
|
16
|
+
|
17
|
+
You can also specify which attributes you'd like to use instead of the defaults
|
18
|
+
|
19
|
+
class User < ActiveRecord::Base
|
20
|
+
include Txter::Contactable
|
21
|
+
|
22
|
+
sms_phone_number_column :mobile_number
|
23
|
+
sms_blocked_column :is_sms_blocked
|
24
|
+
sms_confirmation_code_column :the_sms_confirmation_code
|
25
|
+
sms_confirmation_attempted_column :when_was_the_sms_confirmation_attempted
|
26
|
+
sms_confirmed_phone_number_column :the_mobile_number_thats_been_confirmed
|
27
|
+
|
28
|
+
# Defaults to the name on the left (minus the '_column' at the end)
|
29
|
+
end
|
30
|
+
|
31
|
+
Turning the thing on
|
32
|
+
---
|
33
|
+
|
34
|
+
Because it can be expensive to send TXTs accidentally, it's required that you manually configure Txter in your app. Put this line in config/environments/production.rb or anything that loads _only_ in your production environment:
|
35
|
+
|
36
|
+
Txter.mode = :live
|
37
|
+
|
38
|
+
Skipping this step (or adding any other value) will prevent TXTs from actually being sent.
|
39
|
+
|
40
|
+
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:
|
41
|
+
|
42
|
+
Txter.configure do |config|
|
43
|
+
# these two are required:
|
44
|
+
# (replace them with your actual account info)
|
45
|
+
config.client_id = 12345
|
46
|
+
config.client_key = 'ABC123'
|
47
|
+
|
48
|
+
# the rest are optional:
|
49
|
+
config.short_code = 00001 # if you have a custom short code
|
50
|
+
config.proxy_address = 'my.proxy.com'
|
51
|
+
config.proxy_port = '80'
|
52
|
+
config.proxy_username = 'user'
|
53
|
+
config.proxy_password = 'password'
|
54
|
+
end
|
55
|
+
|
56
|
+
Phone number formatting
|
57
|
+
---
|
58
|
+
|
59
|
+
Whatever is stored in the sms_phone_number_column will be subject to normalized formatting:
|
60
|
+
|
61
|
+
user = User.create :sms_phone_number => '(206) 555-1234'
|
62
|
+
user.sms_phone_number # => 2065551234
|
63
|
+
|
64
|
+
If you want to preserve the format of the number exactly as the user entered it you'll want
|
65
|
+
to save that in a different attribute.
|
66
|
+
|
67
|
+
|
68
|
+
Confirming Phone Number And Sending Messages
|
69
|
+
====
|
70
|
+
|
71
|
+
You can manage the user's SMS state like so:
|
72
|
+
|
73
|
+
@user = User.create(:sms_phone_number => '5552223333')
|
74
|
+
@user.send_sms_confirmation!
|
75
|
+
@user.sms_confirmed? # => false
|
76
|
+
|
77
|
+
then ask the user for the confirmation code off their phone and pass it in to sms_confirm_with:
|
78
|
+
|
79
|
+
@user.sms_confirm_with(user_provided_code)
|
80
|
+
|
81
|
+
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:
|
82
|
+
|
83
|
+
@user.sms_confirmed? # => true
|
84
|
+
@user.send_sms!("Hi! This is a text message.")
|
85
|
+
|
86
|
+
Then maybe the user will reply with 'BLOCK' by accident and @user.sms_blocked? will be true.
|
87
|
+
You can fix this by calling:
|
88
|
+
|
89
|
+
@user.unblock_sms!
|
90
|
+
|
91
|
+
|
92
|
+
Receiving Messages From 4info.com
|
93
|
+
====
|
94
|
+
|
95
|
+
You can also receive data posted to you from 4info.com. This is how you'll receive messages and notices that users have been blocked.
|
96
|
+
All you need is to create a bare controller and include Txter::Controller into it. Then specify which Ruby class you're using as a contactable user model (likely User)
|
97
|
+
|
98
|
+
|
99
|
+
class SMSController < ApplicationController
|
100
|
+
include Txter::Controller
|
101
|
+
|
102
|
+
sms_contactable User # or whichever class you included Txter::Contactable into
|
103
|
+
end
|
104
|
+
|
105
|
+
And hook this up in your routes.rb file like so:
|
106
|
+
|
107
|
+
ActionController::Routing::Routes.draw do |map|
|
108
|
+
map.route '4info', :controller => 'txter', :action => :index
|
109
|
+
end
|
110
|
+
|
111
|
+
Now just tell 4info.com to POST messages and block notices to you at:
|
112
|
+
|
113
|
+
http://myrubyapp.com/4info
|
114
|
+
|
115
|
+
Now if your users reply to an SMS with 'STOP' your database will be updated to reflect this.
|
116
|
+
|
117
|
+
Incoming messages from a user will automatically be sent to that user's record:
|
118
|
+
|
119
|
+
# If "I love you!" is sent to you from a user with the phone
|
120
|
+
# number "555-111-9999" then the following will be executed:
|
121
|
+
User.find_by_sms_phone_number('5551119999').receive_sms("I love you!")
|
122
|
+
|
123
|
+
That's it. Patches welcome, forks celebrated.
|
124
|
+
|
125
|
+
Copyright (c) 2010 [Jack Danger Canty](http://jåck.com/), released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "txter"
|
8
|
+
gem.summary = %Q{Send and receive SMS messages simply via the Twilio gateway or the 4info.com gateway}
|
9
|
+
gem.description = %Q{Drop-in functionality to let Ruby apps send and receive TXT messages}
|
10
|
+
gem.email = "gitcommit@6brand.com"
|
11
|
+
gem.homepage = "http://github.com/JackDanger/txter"
|
12
|
+
gem.authors = ["Jack Danger Canty"]
|
13
|
+
# gem.add_dependency "hpricot", ">= 0"
|
14
|
+
# gem.add_dependency "haml", ">= 0"
|
15
|
+
gem.add_development_dependency "shoulda", ">= 0"
|
16
|
+
gem.add_development_dependency "mocha", ">= 0"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
desc "Test Txter"
|
26
|
+
Rake::TestTask.new(:test) do |test|
|
27
|
+
test.libs << 'lib' << 'test'
|
28
|
+
test.pattern = 'test/**/*_test.rb'
|
29
|
+
test.verbose = true
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
require 'rcov/rcovtask'
|
34
|
+
Rcov::RcovTask.new do |test|
|
35
|
+
test.libs << 'test'
|
36
|
+
test.pattern = 'test/**/test_*.rb'
|
37
|
+
test.verbose = true
|
38
|
+
end
|
39
|
+
rescue LoadError
|
40
|
+
task :rcov do
|
41
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
task :test => :check_dependencies
|
46
|
+
|
47
|
+
task :default => :test
|
48
|
+
|
49
|
+
require 'rake/rdoctask'
|
50
|
+
Rake::RDocTask.new do |rdoc|
|
51
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
52
|
+
|
53
|
+
rdoc.rdoc_dir = 'rdoc'
|
54
|
+
rdoc.title = "inline_styles #{version}"
|
55
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.2
|
data/init.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
!!! XML
|
2
|
+
%request{ :clientId => config.client_id, :clientKey => config.client_key, :type => "MESSAGE" }
|
3
|
+
%message
|
4
|
+
- if config.short_code
|
5
|
+
%sender
|
6
|
+
%type= 6
|
7
|
+
%id= config.short_code
|
8
|
+
%recipient
|
9
|
+
%type= 5
|
10
|
+
%id= number
|
11
|
+
%text= message.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub("'", "'").gsub('"', """)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Txter
|
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
|
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 :gateway
|
30
|
+
attr_accessor :short_code
|
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,150 @@
|
|
1
|
+
module Txter
|
2
|
+
module Contactable
|
3
|
+
|
4
|
+
Attributes = [ :sms_phone_number,
|
5
|
+
:sms_blocked,
|
6
|
+
:sms_confirmation_code,
|
7
|
+
:sms_confirmation_attempted,
|
8
|
+
:sms_confirmed_phone_number ]
|
9
|
+
|
10
|
+
def self.included(model)
|
11
|
+
gem 'haml'
|
12
|
+
require 'haml'
|
13
|
+
require 'net/http'
|
14
|
+
|
15
|
+
Attributes.each do |attribute|
|
16
|
+
# add a method in the class for setting or retrieving
|
17
|
+
# which column should be used for which attribute
|
18
|
+
#
|
19
|
+
# :sms_phone_number_column defaults to :sms_phone_number, etc.
|
20
|
+
model.instance_eval "
|
21
|
+
def #{attribute}_column(value = nil)
|
22
|
+
@#{attribute}_column ||= :#{attribute}
|
23
|
+
@#{attribute}_column = value if value
|
24
|
+
@#{attribute}_column
|
25
|
+
end
|
26
|
+
"
|
27
|
+
# provide helper methods to access the right value
|
28
|
+
# no matter which column it's stored in.
|
29
|
+
#
|
30
|
+
# e.g.: @user.txter_sms_confirmation_code
|
31
|
+
# == @user.send(User.sms_confirmation_code_column)
|
32
|
+
model.class_eval "
|
33
|
+
def txter_#{attribute}
|
34
|
+
send self.class.#{attribute}_column
|
35
|
+
end
|
36
|
+
alias_method :txter_#{attribute}?, :txter_#{attribute}
|
37
|
+
def txter_#{attribute}=(value)
|
38
|
+
send self.class.#{attribute}_column.to_s+'=', value
|
39
|
+
end
|
40
|
+
"
|
41
|
+
end
|
42
|
+
|
43
|
+
# normalize the phone number before it's saved in the database
|
44
|
+
# (only for model classes using callbacks a la ActiveRecord,
|
45
|
+
# other folks will have to do this by hand)
|
46
|
+
if model.respond_to?(:before_save)
|
47
|
+
model.before_save :normalize_sms_phone_number
|
48
|
+
model.class_eval do
|
49
|
+
def normalize_sms_phone_number
|
50
|
+
self.txter_sms_phone_number = Txter.numerize(txter_sms_phone_number)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Sends one or more TXT messages to the contactable record's
|
57
|
+
# mobile number (if the number has been confirmed).
|
58
|
+
# Any messages longer than 160 characters will need to be accompanied
|
59
|
+
# by a second argument <tt>true</tt> to clarify that sending
|
60
|
+
# multiple messages is intentional.
|
61
|
+
def send_sms!(msg, allow_multiple = false)
|
62
|
+
if msg.to_s.size > 160 && !allow_multiple
|
63
|
+
raise ArgumentError, "SMS Message is too long. Either specify that you want multiple messages or shorten the string."
|
64
|
+
end
|
65
|
+
return false if msg.to_s.strip.blank? || txter_sms_blocked?
|
66
|
+
return false unless sms_confirmed?
|
67
|
+
|
68
|
+
# split into pieces that fit as individual messages.
|
69
|
+
msg.to_s.scan(/.{1,160}/m).map do |text|
|
70
|
+
if Txter.deliver(text, txter_sms_phone_number).success?
|
71
|
+
text.size
|
72
|
+
else
|
73
|
+
false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Sends an SMS validation request through the gateway
|
79
|
+
def send_sms_confirmation!
|
80
|
+
return false if txter_sms_blocked?
|
81
|
+
return true if sms_confirmed?
|
82
|
+
return false if txter_sms_phone_number.blank?
|
83
|
+
|
84
|
+
confirmation_code = Txter.generate_confirmation_code
|
85
|
+
|
86
|
+
# Use this class' confirmation_message method if it
|
87
|
+
# exists, otherwise use the generic message
|
88
|
+
message = (self.class.respond_to?(:confirmation_message) ?
|
89
|
+
self.class :
|
90
|
+
Txter).confirmation_message(confirmation_code)
|
91
|
+
|
92
|
+
if message.to_s.size > 160
|
93
|
+
raise ArgumentError, "SMS Confirmation Message is too long. Limit it to 160 characters of unescaped text."
|
94
|
+
end
|
95
|
+
|
96
|
+
response = Txter.deliver(message, txter_sms_phone_number)
|
97
|
+
|
98
|
+
if response.success?
|
99
|
+
update_txter_sms_confirmation confirmation_code
|
100
|
+
else
|
101
|
+
false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Sends an unblock request via xml to the 4info gateway.
|
107
|
+
# If request succeeds, changes the contactable record's
|
108
|
+
# sms_blocked_column to false.
|
109
|
+
def unblock_sms!
|
110
|
+
return false unless txter_sms_blocked?
|
111
|
+
|
112
|
+
response = Txter.unblock(txter_sms_phone_number)
|
113
|
+
if response.success?
|
114
|
+
self.txter_sms_blocked = 'false'
|
115
|
+
save
|
116
|
+
else
|
117
|
+
false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Compares user-provided code with the stored confirmation
|
122
|
+
# code. If they match then the current phone number is set
|
123
|
+
# as confirmed by the user.
|
124
|
+
def sms_confirm_with(code)
|
125
|
+
if txter_sms_confirmation_code.to_s.downcase == code.downcase
|
126
|
+
# save the phone number into the 'confirmed phone number' attribute
|
127
|
+
self.txter_sms_confirmed_phone_number = txter_sms_phone_number
|
128
|
+
save
|
129
|
+
else
|
130
|
+
false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns true if the current phone number has been confirmed by
|
135
|
+
# the user for recieving TXT messages.
|
136
|
+
def sms_confirmed?
|
137
|
+
return false if txter_sms_confirmed_phone_number.blank?
|
138
|
+
txter_sms_confirmed_phone_number == txter_sms_phone_number
|
139
|
+
end
|
140
|
+
|
141
|
+
protected
|
142
|
+
|
143
|
+
def update_txter_sms_confirmation(new_code)
|
144
|
+
self.txter_sms_confirmation_code = new_code
|
145
|
+
self.txter_sms_confirmation_attempted = Time.now.utc
|
146
|
+
self.txter_sms_confirmed_phone_number = nil
|
147
|
+
save
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/controller.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module Txter
|
2
|
+
module Controller
|
3
|
+
|
4
|
+
def self.included(controller)
|
5
|
+
controller.instance_eval do
|
6
|
+
# the user should specify which class gets contacted
|
7
|
+
def sms_contactable(klass)
|
8
|
+
@@contactable_class = klass
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# the likely default
|
14
|
+
def index
|
15
|
+
recieve_xml
|
16
|
+
end
|
17
|
+
|
18
|
+
# in case this is hooked up as a RESTful route
|
19
|
+
def create
|
20
|
+
recieve_xml
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def recieve_xml
|
26
|
+
|
27
|
+
unless defined?(@@contactable_class)
|
28
|
+
raise RuntimeError, "Please define your user class in the Txter controller via the 'sms_contactable' method"
|
29
|
+
end
|
30
|
+
|
31
|
+
request = params[:request]
|
32
|
+
render :text => 'unknown format', :status => 500 and return unless request
|
33
|
+
case request['type']
|
34
|
+
when 'BLOCK'
|
35
|
+
@contactable = find_contactable(request[:block][:recipient][:id])
|
36
|
+
@contactable.txter_sms_blocked = true
|
37
|
+
@contactable.save!
|
38
|
+
when 'MESSAGE'
|
39
|
+
@contactable = find_contactable(request[:message][:sender][:id])
|
40
|
+
if @contactable.respond_to?(:receive_sms)
|
41
|
+
@contactable.receive_sms(request[:message][:text])
|
42
|
+
else
|
43
|
+
warn "An SMS message was received but #{@@contactable_class.name.inspect} doesn't have a receive_sms method!"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
render :text => 'OK', :status => 200
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_contactable(id)
|
50
|
+
[id, id.sub(/^\+/,''), id.sub(/^\+1/,'')].uniq.compact.each do |possible_phone_number|
|
51
|
+
found = @@contactable_class.find(
|
52
|
+
:first,
|
53
|
+
:conditions =>
|
54
|
+
{ @@contactable_class.sms_phone_number_column => possible_phone_number }
|
55
|
+
)
|
56
|
+
return found if found
|
57
|
+
end
|
58
|
+
# rescue => error
|
59
|
+
# render :text => error.inspect
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|