textmagic 0.2.0

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.
@@ -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