txter 2.0.2

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,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,3 @@
1
+ # Include hook code here
2
+
3
+ require 'txter'
@@ -0,0 +1,6 @@
1
+ !!! XML
2
+ %request{ :clientId => config.client_id, :clientKey => config.client_key, :type => "VALIDATION" }
3
+ %validation
4
+ %recipient
5
+ %type= 5
6
+ %id= number
@@ -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("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub("'", "&apos;").gsub('"', "&quot;")
@@ -0,0 +1,6 @@
1
+ !!! XML
2
+ %request{ :clientId => config.client_id, :clientKey => config.client_key, :type => "UNBLOCK" }
3
+ %unblock
4
+ %recipient
5
+ %type= 5
6
+ %id= number
@@ -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
@@ -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