dimiro1-clickatell 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,51 @@
1
+ == 0.7.0
2
+
3
+ * Added support for sending SMS to multiple recipients.
4
+
5
+ == 0.6.0
6
+
7
+ * API host can now be customized (John Gibbons)
8
+ * Fixed bug with the ampersands in messages, they are now escaped properly (John Brice)
9
+ * Removed reliance on Object#blank? which was causing some compatibility issues (Greg Bell)
10
+ * Added :callback option support (Greg Bell)
11
+ * Added new test mode (Pivotal Labs)
12
+
13
+ == 0.5.0
14
+
15
+ * Added support for mobile originated flag (courtesy Dan Weinand)
16
+ * Added support for WAP push (experimental, courtesy Zhao Lu)
17
+ * Updated specs to use Mocha instead of the built-in RSpec mocking
18
+ * Improved specs in general
19
+
20
+ == 0.4.1
21
+
22
+ * Custom alphanumeric sender would not always be supported by default unless it was explicitly enabled using the req_feat parameter.
23
+
24
+ == 0.4.0
25
+
26
+ * Added API debug mode and --debug option to sms utility
27
+ * Restructured API classes into individual files
28
+ * Refactored command execution into a separate object (CommandExecutor).
29
+ * Major refactoring of API module - converted it to a class with API methods implemented as instance methods. Code is much cleaner and Connection class becomes redundant. See updated documentation.
30
+
31
+ == 0.3.0
32
+
33
+ * Display proper message status, not just the code
34
+ * Make it possible to specify custom :from number/name using SMS utility
35
+ * Added support for custom :from number/name when sending a message.
36
+ * Added support for checking message status to sms utility
37
+ * sms utility now returns the id of a successfully sent message.
38
+ * Capture API KEY errors.
39
+ * Make sure errors are handled when using sms utility to check balance.
40
+
41
+ == 0.2.0
42
+
43
+ * Added Clickatell API error handling to API and sms utility.
44
+ * Handle required/optional arguments for sms utility correctly
45
+ * Added further sms utility usage information to website
46
+ * Make sure sms utility gracefully handles missing recipient/message
47
+ * Added balance query support to API and SMS utility (--check-balance).
48
+
49
+ == 0.1.0 2007-08-17
50
+
51
+ * Initial release.
data/License.txt ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2007 Luke Redpath
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.
21
+
data/RDOC_README.txt ADDED
@@ -0,0 +1,33 @@
1
+ = Clickatell SMS Library
2
+
3
+ To use this gem, you will need sign up for an account at www.clickatell.com. Once you are registered and logged into your account centre, you should add an HTTP API connection to your account. This will give you your API_ID.
4
+
5
+ == Basic Usage
6
+
7
+ You will need your API_ID as well as your account username and password.
8
+
9
+ require 'rubygems'
10
+ require 'clickatell'
11
+
12
+ api = Clickatell::API.authenticate('your_api_id', 'your_username', 'your_password')
13
+ api.send_message('447771234567', 'Hello from clickatell')
14
+
15
+
16
+ == Command-line SMS Utility
17
+
18
+ The Clickatell gem also comes with a command-line utility that will allow you to send an SMS directly from the command-line.
19
+
20
+ You will need to create a YAML configuration file in your home directory, in a file called .clickatell that resembles the following:
21
+
22
+ # ~/.clickatell
23
+ api_key: your_api_id
24
+ username: your_username
25
+ password: your_password
26
+
27
+ You can then use the sms utility to send a message to a single recipient:
28
+
29
+ sms 447771234567 'Hello from clickatell'
30
+
31
+ Alternatively, you can specify the username and password as a command line option. Run +sms+ without any arguments for a full list of options.
32
+
33
+ See http://clickatell.rubyforge.org for further instructions.
data/README.textile ADDED
@@ -0,0 +1,40 @@
1
+ h1. Clickatell SMS Library
2
+
3
+ To use this gem, you will need sign up for an account at www.clickatell.com. Once you are registered and logged into your account centre, you should add an HTTP API connection to your account. This will give you your API_ID.
4
+
5
+ h2. Basic Usage
6
+
7
+ You will need your API_ID as well as your account username and password.
8
+
9
+ require 'rubygems'
10
+ require 'clickatell'
11
+
12
+ api = Clickatell::API.authenticate('your_api_id', 'your_username', 'your_password')
13
+ api.send_message('447771234567', 'Hello from clickatell')
14
+
15
+ To send a message to multiple recipients, simply pass in an array of numbers.
16
+
17
+ api.send_message(['447771234567', '447771234568'], 'Hello from clickatell')
18
+
19
+ h2. Command-line SMS Utility
20
+
21
+ The Clickatell gem also comes with a command-line utility that will allow you to send an SMS directly from the command-line.
22
+
23
+ You will need to create a YAML configuration file in your home directory, in a file called .clickatell that resembles the following:
24
+
25
+ # ~/.clickatell
26
+ api_key: your_api_id
27
+ username: your_username
28
+ password: your_password
29
+
30
+ You can then use the sms utility to send a message to a single recipient:
31
+
32
+ sms 447771234567 'Hello from clickatell'
33
+
34
+ Alternatively, you can specify the username and password as a command line option. Run +sms+ without any arguments for a full list of options.
35
+
36
+ The sms utility also supports multiple, comma-separated recipients (up to 100).
37
+
38
+ sms 447771111111,447772222222 "Hello everyone"
39
+
40
+ See http://clickatell.rubyforge.org for further instructions.
data/bin/sms ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ local_libs = [
4
+ File.join(File.dirname(__FILE__), *%w[../lib/clickatell]),
5
+ File.join(File.dirname(__FILE__), *%w[../lib/clickatell/utility])
6
+ ]
7
+
8
+ if File.exist?(local_libs.first)
9
+ local_libs.each { |lib| require lib }
10
+ else
11
+ require 'rubygems'
12
+ require 'clickatell'
13
+ require 'clickatell/utility'
14
+ end
15
+
16
+ # parse command line options
17
+ options = Clickatell::Utility::Options.parse(ARGV)
18
+
19
+ # authenticate and load the API
20
+ api = Clickatell::API.authenticate(options.api_key, options.username, options.password)
21
+
22
+ begin
23
+ if options.show_balance
24
+ puts "Retrieving account balance..."
25
+ puts "You have #{api.account_balance} credits remaining."
26
+ exit 0
27
+ elsif options.show_status
28
+ puts "Getting status of message ##{options.message_id}."
29
+ status = api.message_status(options.message_id)
30
+ puts "Status: #{Clickatell::API::MessageStatus[status]} (##{status})."
31
+ exit 0
32
+ else
33
+ puts "Sending '#{options.message}' to #{[options.recipient].flatten.join(", ")}..."
34
+ additional_opts = {}
35
+ additional_opts[:from] = options.from if options.from
36
+ msg_id = api.send_message(options.recipient, options.message, additional_opts)
37
+ puts "Message sent successfully (message id: #{[msg_id].flatten.join(", ")})."
38
+ exit 0
39
+ end
40
+
41
+ rescue Clickatell::API::Error => e
42
+ case e.code
43
+ when '001', '002', '003', '005', '108'
44
+ puts "Authentication failed. Please check your username, password and API key and try again."
45
+ exit 1
46
+ when '004'
47
+ puts "Your account has been frozen. Please contact Clickatell support."
48
+ exit 1
49
+ when '007'
50
+ puts "Requests for this API key are not permitted from this IP address."
51
+ exit 1
52
+ else
53
+ puts "Unexpected error occurred. #{e.message} (error code: #{e.code})."
54
+ puts "Please contact the author (contact@lukeredpath.co.uk) with the above error."
55
+ exit 1
56
+ end
57
+
58
+ rescue Timeout::Error
59
+ puts "The connection the the Clickatell service timed out"
60
+ puts "Please check your network settings and try again."
61
+ exit 1
62
+ end
data/lib/clickatell.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Clickatelll end
2
+
3
+ %w( core-ext/hash
4
+ clickatell/version
5
+ clickatell/api
6
+ clickatell/response
7
+
8
+ ).each do |lib|
9
+ require File.join(File.dirname(__FILE__), lib)
10
+ end
@@ -0,0 +1,148 @@
1
+ module Clickatell
2
+ # This module provides the core implementation of the Clickatell
3
+ # HTTP service.
4
+ class API
5
+ attr_accessor :auth_options
6
+
7
+ class << self
8
+ # Authenticates using the given credentials and returns an
9
+ # API instance with the authentication options set to use the
10
+ # resulting session_id.
11
+ def authenticate(api_id, username, password)
12
+ api = self.new
13
+ session_id = api.authenticate(api_id, username, password)
14
+ api.auth_options = { :session_id => session_id }
15
+ api
16
+ end
17
+
18
+ # Set to true to enable debugging (off by default)
19
+ attr_accessor :debug_mode
20
+
21
+ # Enable secure mode (SSL)
22
+ attr_accessor :secure_mode
23
+
24
+ # Allow customizing URL
25
+ attr_accessor :api_service_host
26
+
27
+ # Set to true to test message sending; this will not actually send
28
+ # messages but will collect sent messages in a testable collection.
29
+ # (off by default)
30
+ attr_accessor :test_mode
31
+ end
32
+ self.debug_mode = false
33
+ self.secure_mode = false
34
+ self.test_mode = false
35
+
36
+ # Creates a new API instance using the specified +auth options+.
37
+ # +auth_options+ is a hash containing either a :session_id or
38
+ # :username, :password and :api_key.
39
+ #
40
+ # Some API calls (authenticate, ping etc.) do not require any
41
+ # +auth_options+. +auth_options+ can be updated using the accessor methods.
42
+ def initialize(auth_options={})
43
+ @auth_options = auth_options
44
+ end
45
+
46
+ # Authenticates using the specified credentials. Returns
47
+ # a session_id if successful which can be used in subsequent
48
+ # API calls.
49
+ def authenticate(api_id, username, password)
50
+ response = execute_command('auth', 'http',
51
+ :api_id => api_id,
52
+ :user => username,
53
+ :password => password
54
+ )
55
+ parse_response(response)['OK']
56
+ end
57
+
58
+ # Pings the service with the specified session_id to keep the
59
+ # session alive.
60
+ def ping(session_id)
61
+ execute_command('ping', 'http', :session_id => session_id)
62
+ end
63
+
64
+ # Sends a message +message_text+ to +recipient+. Recipient
65
+ # number should have an international dialing prefix and
66
+ # no leading zeros (unless you have set a default prefix
67
+ # in your clickatell account centre).
68
+ #
69
+ # Additional options:
70
+ # :from - the from number/name
71
+ # :set_mobile_originated - mobile originated flag
72
+ #
73
+ # Returns a new message ID if successful.
74
+ def send_message(recipient, message_text, opts={})
75
+ valid_options = opts.only(:from, :mo, :callback, :cliMsgId)
76
+ valid_options.merge!(:req_feat => '48') if valid_options[:from]
77
+ valid_options.merge!(:mo => '1') if opts[:set_mobile_originated]
78
+ recipient = recipient.join(",")if recipient.is_a?(Array)
79
+ response = execute_command('sendmsg', 'http',
80
+ {:to => recipient, :text => message_text}.merge(valid_options)
81
+ )
82
+ response = parse_response(response)
83
+ response.is_a?(Array) ? response.map { |r| r['ID'] } : response['ID']
84
+ end
85
+
86
+ def send_wap_push(recipient, media_url, notification_text='', opts={})
87
+ valid_options = opts.only(:from)
88
+ valid_options.merge!(:req_feat => '48') if valid_options[:from]
89
+ response = execute_command('si_push', 'mms',
90
+ {:to => recipient, :si_url => media_url, :si_text => notification_text, :si_id => 'foo'}.merge(valid_options)
91
+ )
92
+ parse_response(response)['ID']
93
+ end
94
+
95
+ # Returns the status of a message. Use message ID returned
96
+ # from original send_message call.
97
+ def message_status(message_id)
98
+ response = execute_command('querymsg', 'http', :apimsgid => message_id)
99
+ parse_response(response)['Status']
100
+ end
101
+
102
+ # Returns the number of credits remaining as a float.
103
+ def account_balance
104
+ response = execute_command('getbalance', 'http')
105
+ parse_response(response)['Credit'].to_f
106
+ end
107
+
108
+ def sms_requests
109
+ @sms_requests ||= []
110
+ end
111
+
112
+ protected
113
+ def execute_command(command_name, service, parameters={}) #:nodoc:
114
+ executor = CommandExecutor.new(auth_hash, self.class.secure_mode, self.class.debug_mode, self.class.test_mode)
115
+ result = executor.execute(command_name, service, parameters)
116
+
117
+ (sms_requests << executor.sms_requests).flatten! if self.class.test_mode
118
+
119
+ result
120
+ end
121
+
122
+ def parse_response(raw_response) #:nodoc:
123
+ Clickatell::Response.parse(raw_response)
124
+ end
125
+
126
+ def auth_hash #:nodoc:
127
+ if @auth_options[:session_id]
128
+ { :session_id => @auth_options[:session_id] }
129
+ elsif @auth_options[:api_id]
130
+ { :user => @auth_options[:username],
131
+ :password => @auth_options[:password],
132
+ :api_id => @auth_options[:api_key] }
133
+ else
134
+ {}
135
+ end
136
+ end
137
+
138
+ end
139
+ end
140
+
141
+ %w( api/command
142
+ api/command_executor
143
+ api/error
144
+ api/message_status
145
+
146
+ ).each do |lib|
147
+ require File.join(File.dirname(__FILE__), lib)
148
+ end
@@ -0,0 +1,31 @@
1
+ require "cgi"
2
+ module Clickatell
3
+ class API
4
+
5
+ # Represents a Clickatell HTTP gateway command in the form
6
+ # of a complete URL (the raw, low-level request).
7
+ class Command
8
+ API_SERVICE_HOST = 'api.clickatell.com'
9
+
10
+ def initialize(command_name, service = 'http', opts={})
11
+ @command_name = command_name
12
+ @service = service
13
+ @options = { :secure => false }.merge(opts)
14
+ end
15
+
16
+ # Returns a URL for the given parameters (a hash).
17
+ def with_params(param_hash)
18
+ param_string = '?' + param_hash.map { |key, value| "#{::CGI.escape(key.to_s)}=#{::CGI.escape(value.to_s)}" }.sort.join('&')
19
+ return URI.parse(File.join(api_service_uri, @command_name + param_string))
20
+ end
21
+
22
+ protected
23
+ def api_service_uri
24
+ protocol = @options[:secure] ? 'https' : 'http'
25
+ api_service_host = ((Clickatell::API.api_service_host.nil? || Clickatell::API.api_service_host.empty?) ? API_SERVICE_HOST : Clickatell::API.api_service_host)
26
+ return "#{protocol}://#{api_service_host}/#{@service}/"
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,69 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ module Clickatell
5
+ class API
6
+
7
+ class FakeHttpResponse
8
+ def body
9
+ "test"
10
+ end
11
+ end
12
+
13
+ # Used to run commands agains the Clickatell gateway.
14
+ class CommandExecutor
15
+ def initialize(authentication_hash, secure=false, debug=false, test_mode=false)
16
+ @authentication_hash = authentication_hash
17
+ @debug = debug
18
+ @secure = secure
19
+ @test_mode = test_mode
20
+
21
+ allow_request_recording if @test_mode
22
+ end
23
+
24
+ def in_test_mode?
25
+ @test_mode
26
+ end
27
+
28
+ # Builds a command object and sends it using HTTP GET.
29
+ # Will output URLs as they are requested to stdout when
30
+ # debugging is enabled.
31
+ def execute(command_name, service, parameters={})
32
+ request_uri = command(command_name, service, parameters)
33
+ puts "[debug] Sending request to #{request_uri}" if @debug
34
+ get_response(request_uri).first
35
+ end
36
+
37
+ protected
38
+ def command(command_name, service, parameters) #:nodoc:
39
+ Command.new(command_name, service, :secure => @secure).with_params(
40
+ parameters.merge(@authentication_hash)
41
+ )
42
+ end
43
+
44
+ def get_response(uri)
45
+ if in_test_mode?
46
+ sms_requests << uri
47
+ [FakeHttpResponse.new]
48
+ else
49
+ http = Net::HTTP.new(uri.host, uri.port)
50
+ http.use_ssl = (uri.scheme == 'https')
51
+ http.start do |http|
52
+ resp, body = http.get([uri.path, uri.query].join('?'))
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def allow_request_recording
60
+ class << self
61
+ define_method :sms_requests do
62
+ @sms_requests ||= []
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,25 @@
1
+ module Clickatell
2
+ class API
3
+
4
+ # Clickatell API Error exception.
5
+ class Error < StandardError
6
+ attr_reader :code, :message
7
+
8
+ def initialize(code, message)
9
+ @code, @message = code, message
10
+ end
11
+
12
+ # Creates a new Error from a Clickatell HTTP response string
13
+ # e.g.:
14
+ #
15
+ # Error.parse("ERR: 001, Authentication error")
16
+ # # => #<Clickatell::API::Error code='001' message='Authentication error'>
17
+ def self.parse(error_string)
18
+ error_details = error_string.split(':').last.strip
19
+ code, message = error_details.split(',').map { |s| s.strip }
20
+ self.new(code, message)
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Clickatell
2
+ class API
3
+
4
+ class MessageStatus
5
+ STATUS_MAP = {
6
+ 1 => 'Message unknown',
7
+ 2 => 'Message queued',
8
+ 3 => 'Delivered to gateway',
9
+ 4 => 'Received by recipient',
10
+ 5 => 'Error with message',
11
+ 6 => 'User cancelled messaged delivery',
12
+ 7 => 'Error delivering message',
13
+ 8 => 'OK',
14
+ 9 => 'Routing error',
15
+ 10 => 'Message expired',
16
+ 11 => 'Message queued for later delivery',
17
+ 12 => 'Out of credit'
18
+ }
19
+
20
+ def self.[](code)
21
+ STATUS_MAP[code]
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ require 'yaml'
2
+
3
+ module Clickatell
4
+
5
+ # Used to parse HTTP responses returned from Clickatell API calls.
6
+ class Response
7
+
8
+ class << self
9
+ PARSE_REGEX = /[A-Za-z0-9]+:.*?(?:(?=[A-Za-z0-9]+:)|$)/
10
+
11
+ # Returns the HTTP response body data as a hash.
12
+ def parse(http_response)
13
+ return { 'OK' => 'session_id' } if API.test_mode
14
+
15
+ if http_response.body.scan(/ERR/).any?
16
+ raise Clickatell::API::Error.parse(http_response.body)
17
+ end
18
+ results = http_response.body.split("\n").map do |line|
19
+ # YAML.load converts integer strings that have leading zeroes into integers
20
+ # using octal rather than decimal. This isn't what we want, so we'll strip out any
21
+ # leading zeroes in numbers here.
22
+ response_fields = line.scan(PARSE_REGEX)
23
+ response_fields = response_fields.collect { |field| field.gsub(/\b0+(\d+)\b/, '\1') }
24
+ YAML.load(response_fields.join("\n"))
25
+ end
26
+ results.length == 1 ? results.first : results
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ module Clickatell
2
+ module Utility
3
+ end
4
+ end
5
+
6
+ require File.join(File.dirname(__FILE__), *%w[utility/options])
@@ -0,0 +1,110 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ module Clickatell
5
+ module Utility
6
+ class Options #:nodoc:
7
+ class << self
8
+
9
+ def parse(args)
10
+ @options = self.default_options
11
+ parser = OptionParser.new do |opts|
12
+ opts.banner = "Usage: sms [options] recipient(s) message"
13
+ opts.separator " Recipients can be a comma-separated list, up to 100 max."
14
+ opts.separator ""
15
+ opts.separator "Specific options:"
16
+
17
+ opts.on('-u', '--username USERNAME',
18
+ "Specify the clickatell username (overrides ~/.clickatell setting)") do |username|
19
+ @options.username = username
20
+ end
21
+
22
+ opts.on('-p', '--password PASSWORD',
23
+ "Specify the clickatell password (overrides ~/.clickatell setting)") do |password|
24
+ @options.password = password
25
+ end
26
+
27
+ opts.on('-k', '--apikey API_KEY',
28
+ "Specify the clickatell API key (overrides ~/.clickatell setting)") do |key|
29
+ @options.api_key = key
30
+ end
31
+
32
+ opts.on('-f', '--from NAME_OR_NUMBER',
33
+ "Specify the name or number that the SMS will appear from") do |from|
34
+ @options.from = from
35
+ end
36
+
37
+ opts.on('-b', '--show-balance',
38
+ "Shows the total number of credits remaining on your account") do
39
+ @options.show_balance = true
40
+ end
41
+
42
+ opts.on('-s', '--status MESSAGE_ID',
43
+ "Displays the status of the specified message.") do |message_id|
44
+ @options.message_id = message_id
45
+ @options.show_status = true
46
+ end
47
+
48
+ opts.on('-S', '--secure',
49
+ "Sends request using HTTPS") do
50
+ Clickatell::API.secure_mode = true
51
+ end
52
+
53
+ opts.on('-d', '--debug') do
54
+ Clickatell::API.debug_mode = true
55
+ end
56
+
57
+ opts.on_tail('-h', '--help', "Show this message") do
58
+ puts opts
59
+ exit
60
+ end
61
+
62
+ opts.on_tail('-v', '--version') do
63
+ puts "Ruby Clickatell SMS Utility #{Clickatell::VERSION}"
64
+ exit
65
+ end
66
+ end
67
+
68
+ parser.parse!(args)
69
+ @options.recipient = ARGV[-2]
70
+ @options.message = ARGV[-1]
71
+
72
+ if (@options.message.nil? || @options.recipient.nil?) && send_message?
73
+ puts "You must specify a recipient and message!"
74
+ puts parser
75
+ exit
76
+ end
77
+
78
+ @options.recipient = @options.recipient.split(",")
79
+
80
+ return @options
81
+
82
+ rescue OptionParser::MissingArgument => e
83
+ switch_given = e.message.split(':').last.strip
84
+ puts "The #{switch_given} option requires an argument."
85
+ puts parser
86
+ exit
87
+ end
88
+
89
+ def default_options
90
+ options = OpenStruct.new
91
+ config_file = File.open(File.join(ENV['HOME'], '.clickatell'))
92
+ config = YAML.load(config_file)
93
+ options.username = config['username']
94
+ options.password = config['password']
95
+ options.api_key = config['api_key']
96
+ options.from = config['from']
97
+ return options
98
+ rescue Errno::ENOENT
99
+ return options
100
+ end
101
+
102
+ def send_message?
103
+ (@options.show_status.nil? &&
104
+ @options.show_balance.nil?)
105
+ end
106
+
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,13 @@
1
+ module Clickatell #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 7
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+
9
+ def self.to_s
10
+ STRING
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class Hash
2
+ # Returns a new hash containing only the keys specified
3
+ # that exist in the current hash.
4
+ #
5
+ # {:a => '1', :b => '2', :c => '3'}.only(:a, :c)
6
+ # # => {:a => '1', :c => '3'}
7
+ #
8
+ # Keys that do not exist in the original hash are ignored.
9
+ def only(*keys)
10
+ inject( {} ) do |new_hash, (key, value)|
11
+ new_hash[key] = value if keys.include?(key)
12
+ new_hash
13
+ end
14
+ end
15
+ end
data/spec/api_spec.rb ADDED
@@ -0,0 +1,239 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/../lib/clickatell'
3
+
4
+ class FakeHttp
5
+ def start(&block)
6
+ yield self
7
+ end
8
+ end
9
+
10
+ module Clickatell
11
+
12
+ describe "API Command" do
13
+ before do
14
+ @command = API::Command.new('cmdname')
15
+ end
16
+
17
+ after do
18
+ Clickatell::API.api_service_host = nil
19
+ end
20
+
21
+ it "should return encoded URL for the specified command and parameters" do
22
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
23
+ url.should == URI.parse("http://api.clickatell.com/http/cmdname?param_one=abc&param_two=123")
24
+ end
25
+
26
+ it "should URL encode any special characters in parameters" do
27
+ url = @command.with_params(:param_one => 'abc', :param_two => 'hello world & goodbye cruel world <grin>')
28
+ url.should == URI.parse("http://api.clickatell.com/http/cmdname?param_one=abc&param_two=hello+world+%26+goodbye+cruel+world+%3Cgrin%3E")
29
+ end
30
+
31
+ it "should use a custom host when constructing command URLs if specified" do
32
+ Clickatell::API.api_service_host = 'api.clickatell-custom.co.uk'
33
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
34
+ url.should == URI.parse("http://api.clickatell-custom.co.uk/http/cmdname?param_one=abc&param_two=123")
35
+ end
36
+
37
+ it "should use the default host if specified custom host is nil" do
38
+ Clickatell::API.api_service_host = nil
39
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
40
+ url.should == URI.parse("http://api.clickatell.com/http/cmdname?param_one=abc&param_two=123")
41
+ end
42
+
43
+ it "should use the default host if specified custom host is an empty string" do
44
+ Clickatell::API.api_service_host = ''
45
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
46
+ url.should == URI.parse("http://api.clickatell.com/http/cmdname?param_one=abc&param_two=123")
47
+ end
48
+ end
49
+
50
+ describe "Secure API Command" do
51
+ before do
52
+ @command = API::Command.new('cmdname', 'http', :secure => true)
53
+ end
54
+
55
+ it "should use HTTPS" do
56
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
57
+ url.should == URI.parse("https://api.clickatell.com/http/cmdname?param_one=abc&param_two=123")
58
+ end
59
+ end
60
+
61
+ describe "Command executor" do
62
+ it "should create an API command with the given params" do
63
+ executor = API::CommandExecutor.new(:session_id => '12345')
64
+ executor.stubs(:get_response).returns([])
65
+ API::Command.expects(:new).with('cmdname', 'http', :secure => false).returns(command = stub('Command'))
66
+ command.expects(:with_params).with(:param_one => 'foo', :session_id => '12345').returns(stub_everything('URI'))
67
+ executor.execute('cmdname', 'http', :param_one => 'foo')
68
+ end
69
+
70
+ it "should send the URI generated by the created command via HTTP get and return the response" do
71
+ executor = API::CommandExecutor.new(:session_id => '12345')
72
+ command_uri = URI.parse('http://clickatell.com:8080/path?foo=bar')
73
+ API::Command.stubs(:new).returns(command = stub('Command', :with_params => command_uri))
74
+ Net::HTTP.stubs(:new).with(command_uri.host, command_uri.port).returns(http = FakeHttp.new)
75
+ http.stubs(:use_ssl=)
76
+ http.stubs(:get).with('/path?foo=bar').returns([response = stub('HTTP Response'), 'response body'])
77
+ executor.execute('cmdname', 'http').should == response
78
+ end
79
+
80
+ it "should send the command over a secure HTTPS connection if :secure option is set to true" do
81
+ executor = API::CommandExecutor.new({:session_id => '12345'}, secure = true)
82
+ Net::HTTP.stubs(:new).returns(http = mock('HTTP'))
83
+ http.expects(:use_ssl=).with(true)
84
+ http.stubs(:start).returns([])
85
+ executor.execute('cmdname', 'http')
86
+ end
87
+ end
88
+
89
+ describe "API" do
90
+ before do
91
+ API.debug_mode = false
92
+ API.secure_mode = false
93
+ API.test_mode = false
94
+
95
+ @executor = mock('command executor')
96
+ @api = API.new(:session_id => '1234')
97
+ API::CommandExecutor.stubs(:new).with({:session_id => '1234'}, false, false, false).returns(@executor)
98
+ end
99
+
100
+ it "should use the api_id, username and password to authenticate and return the new session id" do
101
+ @executor.expects(:execute).with('auth', 'http',
102
+ :api_id => '1234',
103
+ :user => 'joebloggs',
104
+ :password => 'superpass'
105
+ ).returns(response = stub('response'))
106
+ Response.stubs(:parse).with(response).returns('OK' => 'new_session_id')
107
+ @api.authenticate('1234', 'joebloggs', 'superpass').should == 'new_session_id'
108
+ end
109
+
110
+ it "should support ping, using the current session_id" do
111
+ @executor.expects(:execute).with('ping', 'http', :session_id => 'abcdefg').returns(response = stub('response'))
112
+ @api.ping('abcdefg').should == response
113
+ end
114
+
115
+ it "should support sending messages to a specified number, returning the message id" do
116
+ @executor.expects(:execute).with('sendmsg', 'http',
117
+ :to => '4477791234567',
118
+ :text => 'hello world & goodbye'
119
+ ).returns(response = stub('response'))
120
+ Response.stubs(:parse).with(response).returns('ID' => 'message_id')
121
+ @api.send_message('4477791234567', 'hello world & goodbye').should == 'message_id'
122
+ end
123
+
124
+ it "should support sending messages to a multiple numbers, returning the message ids" do
125
+ @executor.expects(:execute).with('sendmsg', 'http',
126
+ :to => '4477791234567,447779999999',
127
+ :text => 'hello world & goodbye'
128
+ ).returns(response = stub('response'))
129
+ Response.stubs(:parse).with(response).returns([{'ID' => 'message_1_id'}, {'ID' => 'message_2_id'}])
130
+ @api.send_message(['4477791234567', '447779999999'], 'hello world & goodbye').should == ['message_1_id', 'message_2_id']
131
+ end
132
+
133
+ it "should set the :from parameter and set the :req_feat to 48 when using a custom from string when sending a message" do
134
+ @executor.expects(:execute).with('sendmsg', 'http', has_entries(:from => 'LUKE', :req_feat => '48')).returns(response = stub('response'))
135
+ Response.stubs(:parse).with(response).returns('ID' => 'message_id')
136
+ @api.send_message('4477791234567', 'hello world', :from => 'LUKE')
137
+ end
138
+
139
+ it "should set the :mo flag to 1 when :set_mobile_originated is true when sending a message" do
140
+ @executor.expects(:execute).with('sendmsg', 'http', has_entry(:mo => '1')).returns(response=mock('response'))
141
+ Response.stubs(:parse).with(response).returns('ID' => 'message_id')
142
+ @api.send_message('4477791234567', 'hello world', :set_mobile_originated => true)
143
+ end
144
+
145
+ it "should set the callback flag to the number passed in the options hash" do
146
+ @executor.expects(:execute).with('sendmsg', 'http', has_entry(:callback => 1)).returns(response=mock('response'))
147
+ Response.stubs(:parse).with(response).returns('ID' => 'message_id')
148
+ @api.send_message('4477791234567', 'hello world', :callback => 1)
149
+ end
150
+
151
+ it "should ignore any invalid parameters when sending a message" do
152
+ @executor.expects(:execute).with('sendmsg', 'http', Not(has_key(:any_old_param))).returns(response = stub('response'))
153
+ Response.stubs(:parse).returns('ID' => 'foo')
154
+ @api.send_message('4477791234567', 'hello world', :from => 'LUKE', :any_old_param => 'test')
155
+ end
156
+
157
+ it "should support message status query for a given message id, returning the message status" do
158
+ @executor.expects(:execute).with('querymsg', 'http', :apimsgid => 'messageid').returns(response = stub('response'))
159
+ Response.expects(:parse).with(response).returns('ID' => 'message_id', 'Status' => 'message_status')
160
+ @api.message_status('messageid').should == 'message_status'
161
+ end
162
+
163
+ it "should support balance query, returning number of credits as a float" do
164
+ @executor.expects(:execute).with('getbalance', 'http', {}).returns(response=mock('response'))
165
+ Response.stubs(:parse).with(response).returns('Credit' => '10.0')
166
+ @api.account_balance.should == 10.0
167
+ end
168
+
169
+ it "should raise an API::Error if the response parser raises" do
170
+ @executor.stubs(:execute)
171
+ Response.stubs(:parse).raises(Clickatell::API::Error.new('', ''))
172
+ proc { @api.account_balance }.should raise_error(Clickatell::API::Error)
173
+ end
174
+ end
175
+
176
+ describe API, ' when authenticating' do
177
+ it "should authenticate to retrieve a session_id and return a new API instance using that session id" do
178
+ API.stubs(:new).returns(api = mock('api'))
179
+ api.stubs(:authenticate).with('my_api_key', 'joebloggs', 'mypassword').returns('new_session_id')
180
+ api.expects(:auth_options=).with(:session_id => 'new_session_id')
181
+ API.authenticate('my_api_key', 'joebloggs', 'mypassword')
182
+ end
183
+ end
184
+
185
+ describe API, ' with no authentication options set' do
186
+ it "should build commands with no authentication options" do
187
+ api = API.new
188
+ API::CommandExecutor.stubs(:new).with({}, false, false, false).returns(executor = stub('command executor'))
189
+ executor.stubs(:execute)
190
+ api.ping('1234')
191
+ end
192
+ end
193
+
194
+ describe API, ' in secure mode' do
195
+ it "should execute commands securely" do
196
+ API.secure_mode = true
197
+ api = API.new
198
+ API::CommandExecutor.expects(:new).with({}, true, false, false).returns(executor = stub('command executor'))
199
+ executor.stubs(:execute)
200
+ api.ping('1234')
201
+ end
202
+ end
203
+
204
+ describe "API Error" do
205
+ it "should parse http response string to create error" do
206
+ response_string = "ERR: 001, Authentication error"
207
+ error = Clickatell::API::Error.parse(response_string)
208
+ error.code.should == '001'
209
+ error.message.should == 'Authentication error'
210
+ end
211
+ end
212
+
213
+ describe API, "#test_mode" do
214
+ before(:each) do
215
+ API.secure_mode = false
216
+ API.test_mode = true
217
+ @api = API.new
218
+ end
219
+
220
+ it "should create a new CommandExecutor with test_mode parameter set to true" do
221
+ API::CommandExecutor.expects(:new).with({}, false, false, true).once.returns(executor = mock('command executor'))
222
+ executor.stubs(:execute)
223
+ executor.stubs(:sms_requests).returns([])
224
+ @api.ping('1234')
225
+ end
226
+
227
+ it "should record all commands" do
228
+ @api.ping('1234')
229
+ @api.sms_requests.should_not be_empty
230
+ end
231
+
232
+ it "should return the recorded commands in a flattened array" do
233
+ @api.ping('1234')
234
+ @api.sms_requests.size.should == 1
235
+ @api.sms_requests.first.should_not be_instance_of(Array)
236
+ end
237
+ end
238
+
239
+ end
@@ -0,0 +1,75 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/../lib/clickatell'
3
+
4
+ module Clickatell
5
+ describe "API::CommandExecutor" do
6
+ it "should have test mode" do
7
+ executor = API::CommandExecutor.new({}, false, false, :test_mode)
8
+ executor.should be_in_test_mode
9
+ end
10
+
11
+ it "should default to not test mode" do
12
+ executor = API::CommandExecutor.new({})
13
+ executor.should_not be_in_test_mode
14
+ end
15
+
16
+ describe "#execute" do
17
+ describe "in non-test mode" do
18
+ before(:each) do
19
+ @executor = API::CommandExecutor.new({})
20
+ end
21
+
22
+ it "should not record requests" do
23
+ @executor.should_not respond_to(:sms_requests)
24
+ end
25
+ end
26
+
27
+ describe "in test mode" do
28
+ before(:each) do
29
+ @params = {:foo => 1, :bar => 2}
30
+ @executor = API::CommandExecutor.new(@params, false, false, :test_mode)
31
+ end
32
+
33
+ it "should not make any network calls" do
34
+ Net::HTTP.expects(:new).never
35
+ @executor.execute("foo", "http")
36
+ end
37
+
38
+ it "should start with an empty request collection" do
39
+ @executor.sms_requests.should be_empty
40
+ end
41
+
42
+ it "should record sms requests" do
43
+ @executor.execute("bar", "http")
44
+ @executor.sms_requests.should_not be_empty
45
+ end
46
+
47
+ it "should record a request for each call" do
48
+ @executor.execute("wibble", "http")
49
+ @executor.sms_requests.size.should == 1
50
+ @executor.execute("foozle", "http")
51
+ @executor.sms_requests.size.should == 2
52
+ end
53
+
54
+ it "should return a response that approximates a true Net::HttpResponse" do
55
+ response = @executor.execute("throat-warbler", "http")
56
+ response.body.should == "test"
57
+ end
58
+
59
+ describe "each recorded request" do
60
+ it "should return the command information" do
61
+ command = "rum_tum_tum"
62
+ @executor.execute(command, "http")
63
+
64
+ uri = @executor.sms_requests.first
65
+ uri.host.should == "api.clickatell.com"
66
+ uri.path.should include(command)
67
+ @params.collect { |k, v| "#{k}=#{v}"}.each do |query_parameter|
68
+ uri.query.should include(query_parameter)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,14 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/../lib/clickatell'
3
+
4
+ describe Hash do
5
+ it "should return only the keys specified" do
6
+ hash = {:a => 'foo', :b => 'bar', :c => 'baz'}
7
+ hash.only(:a, :b).should == {:a => 'foo', :b => 'bar'}
8
+ end
9
+
10
+ it "should return only the keys specified, ignoring keys that do not exist" do
11
+ hash = {:a => 'foo', :b => 'bar', :c => 'baz'}
12
+ hash.only(:a, :d).should == {:a => 'foo'}
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/../lib/clickatell'
3
+
4
+ module Clickatell
5
+
6
+ describe "Response parser" do
7
+ before do
8
+ Clickatell::API::Error.stubs(:parse).returns(Clickatell::API::Error.new('', ''))
9
+ end
10
+
11
+ before(:each) do
12
+ API.test_mode = false
13
+ end
14
+
15
+ it "should return hash for one-line success response" do
16
+ Response.parse(stub('response', :body => 'k1: foo k2: bar')).should == {'k1' => 'foo', 'k2' => 'bar'}
17
+ end
18
+
19
+ it "should return array of hashes for multi-line success response" do
20
+ Response.parse(stub('response', :body => "k1: foo\nk2: bar")).should == [{'k1' => 'foo'}, {'k2' => 'bar'}]
21
+ end
22
+
23
+ it "should raise API::Error if response contains an error message" do
24
+ proc { Response.parse(stub('response', :body => 'ERR: 001, Authentication failed')) }.should raise_error(Clickatell::API::Error)
25
+ end
26
+
27
+ {
28
+ '001' => 1, '002' => 2, '003' => 3, '004' => 4,
29
+ '005' => 5, '006' => 6, '007' => 7, '008' => 8,
30
+ '009' => 9, '010' => 10, '011' => 11, '012' => 12
31
+ }.each do |status_str, status_int|
32
+ it "should parse a message status code of #{status_int} when the response body contains a status code of #{status_str}" do
33
+ Response.parse(stub('response', :body => "ID: 0d1d7dda17d5a24edf1555dc0b679d0e Status: #{status_str}")).should == {'ID' => '0d1d7dda17d5a24edf1555dc0b679d0e', 'Status' => status_int}
34
+ end
35
+ end
36
+
37
+ describe "in test mode" do
38
+ before(:each) do
39
+ API.test_mode = true
40
+ end
41
+
42
+ it "should return something approximating a session_id" do
43
+ Response.parse("pretty much anything").should == { 'OK' => 'session_id' }
44
+ end
45
+ end
46
+ end
47
+
48
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'mocha'
4
+ require 'test/unit'
5
+
6
+ Spec::Runner.configure do |config|
7
+ config.mock_with :mocha
8
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dimiro1-clickatell
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.0
5
+ platform: ruby
6
+ authors:
7
+ - Luke Redpath
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-07 00:00:00 -03:00
13
+ default_executable: sms
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description:
26
+ email: luke@lukeredpath.co.uk
27
+ executables:
28
+ - sms
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - RDOC_README.txt
33
+ - History.txt
34
+ - License.txt
35
+ files:
36
+ - History.txt
37
+ - License.txt
38
+ - RDOC_README.txt
39
+ - README.textile
40
+ - bin/sms
41
+ - spec/api_spec.rb
42
+ - spec/command_executor_spec.rb
43
+ - spec/hash_ext_spec.rb
44
+ - spec/response_spec.rb
45
+ - spec/spec.opts
46
+ - spec/spec_helper.rb
47
+ - lib/clickatell/api/command.rb
48
+ - lib/clickatell/api/command_executor.rb
49
+ - lib/clickatell/api/error.rb
50
+ - lib/clickatell/api/message_status.rb
51
+ - lib/clickatell/api.rb
52
+ - lib/clickatell/response.rb
53
+ - lib/clickatell/utility/options.rb
54
+ - lib/clickatell/utility.rb
55
+ - lib/clickatell/version.rb
56
+ - lib/clickatell.rb
57
+ - lib/core-ext/hash.rb
58
+ has_rdoc: true
59
+ homepage: http://clickatell.rubyforge.org
60
+ licenses: []
61
+
62
+ post_install_message:
63
+ rdoc_options:
64
+ - --main
65
+ - RDOC_README.txt
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ requirements: []
81
+
82
+ rubyforge_project: clickatell
83
+ rubygems_version: 1.3.5
84
+ signing_key:
85
+ specification_version: 2
86
+ summary: Ruby interface to the Clickatell SMS gateway service.
87
+ test_files: []
88
+