textmagic 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Vladimir Bobes Tuzinsky
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.
@@ -0,0 +1,40 @@
1
+ = TextMagic
2
+
3
+ TextMagic is a Ruby interface to the TextMagic's HTTP API. You need to have
4
+ a valid TextMagic account to use this gem. Sign up at http://www.textmagic.com
5
+ to get one.
6
+
7
+ == Installation
8
+
9
+ Run
10
+
11
+ gem install textmagic
12
+
13
+ Use +sudo+ if required by your system.
14
+
15
+
16
+ == Basic usage
17
+
18
+ To create an API instance, run:
19
+
20
+ api = TextMagic::API.new(username, password)
21
+
22
+ with your credentials. Created instance will remember the username and password
23
+ and will use it for all commands.
24
+
25
+ To retrieve your account's balance, run:
26
+
27
+ api.account.balance
28
+
29
+ To send a text message, run:
30
+
31
+ api.send 'Hi Vilma!', '441234567890'
32
+
33
+ You can even specify multiple phone numbers:
34
+
35
+ api.send 'Hi Vilma!', '314159265358', '271828182845'
36
+
37
+
38
+ == Copyright
39
+
40
+ Copyright (c) 2009 Vladimir Bobes Tuzinsky. See LICENSE for details.
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "textmagic"
8
+ gem.summary = %Q{Ruby interface to the TextMagic's SMS gateway}
9
+ gem.email = "vladimir.tuzinsky@gmail.com"
10
+ gem.homepage = "http://github.com/bobes/textmagic"
11
+ gem.authors = ["Vladimir Bobes Tuzinsky"]
12
+ gem.rubyforge_project = "textmagic"
13
+ end
14
+
15
+ Jeweler::RubyforgeTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/*_test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+
41
+ task :default => :test
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ if File.exist?('VERSION.yml')
46
+ config = YAML.load(File.read('VERSION.yml'))
47
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "textmagic #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
57
+
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 2
4
+ :patch: 0
@@ -0,0 +1,179 @@
1
+ module TextMagic
2
+
3
+ class API
4
+
5
+ extend Charset
6
+ extend Validation
7
+
8
+ # Creates new API instance with specified credentials. These will be
9
+ # used in all requests to the TextMagic's HTTP gateway done through
10
+ # this instance. Multiple instances with different credentials can
11
+ # be used at the same time.
12
+ #
13
+ # Example usage:
14
+ #
15
+ # api = TextMagic::API.new('fred', 'secret')
16
+ def initialize(username, password)
17
+ @username = username
18
+ @password = password
19
+ end
20
+
21
+ # Executes an account command and returns a hash with account's balance
22
+ # if successful, otherwise it raises an Error.
23
+ # The returned hash will be extended with custom reader method defined
24
+ # in Response module.
25
+ #
26
+ # Example usage:
27
+ #
28
+ # api.account
29
+ # # => { 'balance' => 314.15 }
30
+ #
31
+ # Using custom reader:
32
+ #
33
+ # api.account.balance
34
+ # # => 314.15
35
+ def account
36
+ response = Executor.execute('account', @username, @password)
37
+ response.extend(TextMagic::API::Response::Account)
38
+ end
39
+
40
+ # Executes a send command and returns a hash with message ids, sent text and
41
+ # number of parts if successful, otherwise it raises an Error.
42
+ # The returned hash will be extended with custom reader method defined
43
+ # in Response module.
44
+ #
45
+ # This method accepts any positive number of phone numbers and an additional
46
+ # options hash.
47
+ #
48
+ # The optional parameters you can specify in the options hash are:
49
+ # * +unicode+: accepted values are true, false, 0 and 1
50
+ # * +max_length+: accepted values are nil, 1, 2 and 3, defaults to nil
51
+ # If not specified, the method will determine the unicode value based on the
52
+ # characters in the text.
53
+ #
54
+ # Example usage:
55
+ #
56
+ # api.send('Hi Vilma', '999314159265')
57
+ # # => { 'message_ids' => [141421], 'message_id_hash' => { '999314159265' => '141421' }, 'sent_text' => 'Hi Vilma', 'parts_count' => 1 }
58
+ # api.send(text, phone, :unicode => true)
59
+ # api.send(text, phone1, phone2, :max_length => 2)
60
+ # api.send(text, [phone1, phone2])
61
+ #
62
+ # Using custom readers:
63
+ #
64
+ # response = api.send('Hi Vilma', '999314159265', '999271828182')
65
+ # response.message_ids
66
+ # # => ['141421', '173205']
67
+ # response.message_id_hash
68
+ # # => { '999314159265' => '141421', '999271828182' => '173205' }
69
+ # response.message_id
70
+ # # => '141421'
71
+ # response.message_id('999271828182')
72
+ # # => '173205'
73
+ # response.parts_count
74
+ # # => 1
75
+ def send(text, *args)
76
+ raise Error.new(1, 'Message text is empty') if text.nil? || text.blank?
77
+ options = args.last.is_a?(Hash) ? args.pop : {}
78
+ unicode = API.is_unicode(text)
79
+ options[:unicode] = case options[:unicode]
80
+ when 1, true: 1
81
+ when 0, false: 0
82
+ when nil: unicode ? 1 : 0
83
+ else raise Error.new(10, "Wrong parameter value #{options[:unicode]} for parameter unicode")
84
+ end
85
+ raise Error.new(6, 'Message contains invalid characters') if unicode && options[:unicode] == 0
86
+ raise Error.new(7, 'Message too long') unless API.validate_text_length(text, unicode)
87
+ phones = args.flatten
88
+ raise Error.new(9, 'Invalid phone number format') unless API.validate_phones(phones)
89
+ response = Executor.execute('send', @username, @password, options.merge(:text => text, :phone => phones.join(',')))
90
+ response.extend(TextMagic::API::Response::Send)
91
+ end
92
+
93
+ # Executes a message_status command and returns a hash with states of
94
+ # messages for specified ids if successful, otherwise it raises a
95
+ # TextMagic::API::Error.
96
+ # The returned hash will be extended with custom reader method defined
97
+ # in Response module.
98
+ #
99
+ # This method accepts any positive number of ids specified as an array
100
+ # or as a list of arguments
101
+ #
102
+ # Example usage:
103
+ #
104
+ # api.message_status('141421')
105
+ # # => { '141421' => { 'text' => 'Hi Vilma', 'status' => 'd' , 'created_time' => Mon May 25 16:42:30 +0200 2009, 'reply_number' => '447624800500', 'completed_time' => nil, 'credits_cost' => 0.5 } }
106
+ # api.message_status('141421', '173205')
107
+ # api.message_status(['141421', '173205'])
108
+ #
109
+ # Using custom readers:
110
+ #
111
+ # response = api.message_status('141421', '173205')
112
+ # response['141421'].text
113
+ # # => 'Hi Vilma'
114
+ # response['141421'].status
115
+ # # => 'd'
116
+ # response['141421'].created_time
117
+ # # => Fri May 22 10:10:18 +0200 2009
118
+ def message_status(*ids)
119
+ ids.flatten!
120
+ raise TextMagic::API::Error.new(4, 'Insufficient parameters') if ids.empty?
121
+ response = Executor.execute('message_status', @username, @password, :ids => ids.join(','))
122
+ response.extend(TextMagic::API::Response::MessageStatus)
123
+ end
124
+
125
+ # Executes a receive command and returns a hash with unread messages
126
+ # if successful, otherwise it raises an Error.
127
+ # The returned hash will be extended with custom reader method defined
128
+ # in Response module.
129
+ #
130
+ # This method accepts an optional +last_retrieved_id+ value.
131
+ #
132
+ # Example usage:
133
+ #
134
+ # api.receive
135
+ # # => { 'messages' => [{ 'message_id' => '141421', 'from' => '999314159265', 'timestamp' => Fri May 22 12:12:55 +0200 2009, 'text' => 'Hi Fred!' }], 'unread' => 0 }
136
+ # api.receive '141421'
137
+ #
138
+ # Using custom readers:
139
+ #
140
+ # response = api.receive
141
+ # response.messages
142
+ # # => [{ 'timestamp' => Fri May 22 12:12:55 +0200 2009, 'from' => '999314159265', 'text' => 'Hi Fred', 'message_id' => '141421' }]
143
+ # response.unread
144
+ # # => 0
145
+ # response.messages[0].timestamp
146
+ # # => Fri May 22 12:12:55 +0200 2009
147
+ def receive(last_retrieved_id = nil)
148
+ response = Executor.execute('receive', @username, @password, :last_retrieved_id => last_retrieved_id)
149
+ response.extend(TextMagic::API::Response::Receive)
150
+ end
151
+
152
+ # Executes a delete_reply command and returns a hash with a list of deleted
153
+ # message ids if successful, otherwise it raises an Error.
154
+ # The returned hash will be extended with custom reader method defined
155
+ # in Response module.
156
+ #
157
+ # This method accepts any positive number of ids specified as an array
158
+ # or as a list of arguments.
159
+ #
160
+ # Example usage:
161
+ #
162
+ # api.delete_reply('141421')
163
+ # # => { 'deleted' => ['141421'] }
164
+ # api.delete_reply('141421', '173205')
165
+ # api.delete_reply(['141421', '173205'])
166
+ #
167
+ # Using custom readers:
168
+ #
169
+ # response = api.delete_reply('141421', '173205')
170
+ # response.deleted
171
+ # # => ['141421', '173205']
172
+ def delete_reply(*ids)
173
+ ids.flatten!
174
+ raise TextMagic::API::Error.new(4, 'Insufficient parameters') if ids.empty?
175
+ response = Executor.execute('delete_reply', @username, @password, :ids => ids.join(','))
176
+ response.extend(TextMagic::API::Response::DeleteReply)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,23 @@
1
+ module TextMagic
2
+
3
+ class API
4
+
5
+ module Charset
6
+
7
+ GSM_CHARSET = "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\e\f^{}\\[~]|€ÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà".scan(/./u)
8
+
9
+ # Returns +true+ if the supplied text contains only characters from
10
+ # GSM 03.38 charset, otherwise it returns +false+.
11
+ def is_gsm(text)
12
+ text.scan(/./u).each { |c| return false unless GSM_CHARSET.include?(c) }
13
+ true
14
+ end
15
+
16
+ # Returns +true+ if the supplied text contains characters outside of
17
+ # GSM 03.38 charset, otherwise it returns +false+.
18
+ def is_unicode(text)
19
+ !is_gsm(text)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ module TextMagic
2
+
3
+ class API
4
+
5
+ class Error < StandardError
6
+
7
+ attr_reader :code, :message
8
+
9
+ # Creates an instance of TextMagic::API::Error. Error code and message
10
+ # can be supplied as arguments or as a response hash.
11
+ #
12
+ # TextMagic::API::Error.new(code, message)
13
+ # TextMagic::API::Error.new('error_code' => code, 'error_message' => message)
14
+ def initialize(*args)
15
+ if args.first.is_a?(Hash)
16
+ @code, @message = args.first['error_code'], args.first['error_message']
17
+ else
18
+ @code, @message = args
19
+ end
20
+ end
21
+
22
+ def to_s
23
+ "#{@message} (#{@code})"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ module TextMagic
2
+
3
+ class API
4
+
5
+ class Executor
6
+
7
+ include HTTParty
8
+ base_uri "http://www.textmagic.com/app"
9
+
10
+ # Executes a command by sending a request to the TextMagic's HTTP
11
+ # gateway. This is a low-level generic method used by methods in
12
+ # TextMagic::API class. You should never need to use this method
13
+ # directly.
14
+ #
15
+ # Parameters specified in the +options+ hash will be added to the
16
+ # HTTP request's URI.
17
+ #
18
+ # Returns a hash with values parsed from the server's response if
19
+ # the command was successfully executed. In case the server replies
20
+ # with error, this method raises a TextMagic::API::Error.
21
+ def self.execute(command, username, password, options = {})
22
+ raise TextMagic::API::Error.new(3, 'Command is undefined') if command.nil? || command.blank?
23
+ if username.nil? || username.blank? || password.nil? || password.blank?
24
+ raise TextMagic::API::Error.new(5, 'Invalid username & password combination')
25
+ end
26
+ options.merge!(:username => username, :password => password, :cmd => command)
27
+ options.delete_if { |key, value| key.nil? || key.to_s.blank? || value.nil? || value.to_s.blank? }
28
+ response = self.get('/api', :query => options, :format => :json)
29
+ raise Error.new(response) if response && response['error_code']
30
+ response
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,141 @@
1
+ module TextMagic
2
+
3
+ class API
4
+
5
+ # Used to cleanup response hash and extend it with custom reader methods.
6
+ #
7
+ # === Account response hash
8
+ #
9
+ # When extended, it
10
+ # * converts the +balance+ value to +float+ and
11
+ # * adds a reader method +balance+.
12
+ #
13
+ # === Send response hash
14
+ #
15
+ # When extended, it
16
+ # * inverts the +message_id+ hash and puts it in +message_id_hash+,
17
+ # * adds an array of ids to +message_ids+,
18
+ # * adds reader methods +message_id_hash+, +message_ids+, +sent_text+ and
19
+ # +parts_count+ to the hash.
20
+ # * adds a reader method +message_id+, which returns a +message_id+ for
21
+ # a given phone number, or the first message_id if no phone number
22
+ # is specified.
23
+ #
24
+ # === Message status response hash
25
+ #
26
+ # When extended, it
27
+ # * converts the +credits_cost+ value to +float+,
28
+ # * converts the +created_time+ and +completed_time+ values to +Time+,
29
+ # * adds reader methods +text+, +status+, +reply_number+, +credits_cost+,
30
+ # +created_time+ and +completed_time+ to all values of the hash.
31
+ #
32
+ # === Receive status response hash
33
+ #
34
+ # When extended, it
35
+ # * converts the +timestamp+ value to +Time+,
36
+ # * adds reader methods +messages+ and +unread+ to the hash
37
+ # * adds reader methods +message_id+, +timestamp+, +text+ and +from+
38
+ # to all members of the +messages+ array.
39
+ #
40
+ # === Delete reply response hash
41
+ #
42
+ # When extended, it
43
+ # * adds a reader method +deleted+.
44
+ module Response
45
+
46
+ module Account #:nodoc: all
47
+
48
+ def self.extended(base)
49
+ return unless base.is_a?(Hash)
50
+ base['balance'] = base['balance'].to_f if base['balance']
51
+ end
52
+
53
+ def balance
54
+ self['balance']
55
+ end
56
+ end
57
+
58
+ module Send #:nodoc: all
59
+
60
+ def self.extended(base)
61
+ return unless base.is_a?(Hash)
62
+ base['message_id_hash'] = base.delete('message_id').invert if base['message_id']
63
+ base['message_ids'] = base['message_id_hash'].values.sort if base['message_id_hash']
64
+ end
65
+
66
+ %w(message_id_hash message_ids sent_text parts_count).each do |method|
67
+ module_eval <<-EOS
68
+ def #{method}
69
+ self['#{method}']
70
+ end
71
+ EOS
72
+ end
73
+
74
+ def message_id(phone = nil)
75
+ phone ? message_id_hash[phone] : message_ids.first
76
+ end
77
+ end
78
+
79
+ module MessageStatus #:nodoc: all
80
+
81
+ def self.extended(base)
82
+ return unless base.is_a?(Hash)
83
+ base.values.each do |status|
84
+ status['credits_cost'] = status['credits_cost'].to_f if status['credits_cost']
85
+ status['created_time'] = Time.at(status['created_time'].to_i) if status['created_time']
86
+ status['completed_time'] = Time.at(status['completed_time'].to_i) if status['completed_time']
87
+ status.extend Status
88
+ end
89
+ end
90
+
91
+ module Status
92
+
93
+ %w(text status reply_number credits_cost created_time completed_time).each do |method|
94
+ module_eval <<-EOS
95
+ def #{method}
96
+ self['#{method}']
97
+ end
98
+ EOS
99
+ end
100
+ end
101
+ end
102
+
103
+ module Receive #:nodoc: all
104
+
105
+ def self.extended(base)
106
+ return unless base.is_a?(Hash) && base['messages']
107
+ base['messages'].each do |message|
108
+ message['timestamp'] = Time.at(message['timestamp'].to_i) if message['timestamp']
109
+ message.extend Message
110
+ end
111
+ end
112
+
113
+ %w(messages unread).each do |method|
114
+ module_eval <<-EOS, __FILE__, __LINE__ + 1
115
+ def #{method}
116
+ self['#{method}']
117
+ end
118
+ EOS
119
+ end
120
+
121
+ module Message
122
+
123
+ %w(message_id timestamp text from).each do |method|
124
+ module_eval <<-EOS
125
+ def #{method}
126
+ self['#{method}']
127
+ end
128
+ EOS
129
+ end
130
+ end
131
+ end
132
+
133
+ module DeleteReply #:nodoc: all
134
+
135
+ def deleted
136
+ self['deleted']
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end