roflbot 0.0.5 → 0.0.6

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.
Files changed (36) hide show
  1. data/.gitmodules +3 -0
  2. data/Rakefile +1 -0
  3. data/VERSION +1 -1
  4. data/lib/roflbot.rb +1 -0
  5. data/lib/roflbot/base.rb +42 -25
  6. data/roflbot.gemspec +31 -3
  7. data/test/helper.rb +10 -0
  8. data/test/roflbot/test_base.rb +24 -0
  9. data/vendor/gvoice-ruby/Gemfile +11 -0
  10. data/vendor/gvoice-ruby/README.rdoc +71 -0
  11. data/vendor/gvoice-ruby/Rakefile +12 -0
  12. data/vendor/gvoice-ruby/VERSION +1 -0
  13. data/vendor/gvoice-ruby/bin/gv-notifier +75 -0
  14. data/vendor/gvoice-ruby/bin/gv-place-call +18 -0
  15. data/vendor/gvoice-ruby/bin/gv-send-sms +12 -0
  16. data/vendor/gvoice-ruby/config/gvoice-ruby-config.yml.sample +10 -0
  17. data/vendor/gvoice-ruby/lib/gvoice-ruby.rb +6 -0
  18. data/vendor/gvoice-ruby/lib/gvoice-ruby/call.rb +9 -0
  19. data/vendor/gvoice-ruby/lib/gvoice-ruby/client.rb +264 -0
  20. data/vendor/gvoice-ruby/lib/gvoice-ruby/compatibility.rb +37 -0
  21. data/vendor/gvoice-ruby/lib/gvoice-ruby/config.rb +39 -0
  22. data/vendor/gvoice-ruby/lib/gvoice-ruby/inbox_parser.rb +126 -0
  23. data/vendor/gvoice-ruby/lib/gvoice-ruby/sms.rb +9 -0
  24. data/vendor/gvoice-ruby/lib/gvoice-ruby/user.rb +12 -0
  25. data/vendor/gvoice-ruby/lib/gvoice-ruby/voicemail.rb +10 -0
  26. data/vendor/gvoice-ruby/test/client_test.rb +45 -0
  27. data/vendor/gvoice-ruby/test/config_test.rb +37 -0
  28. data/vendor/gvoice-ruby/test/fixtures/config_fixture.yml +10 -0
  29. data/vendor/gvoice-ruby/test/fixtures/inbox.json +1 -0
  30. data/vendor/gvoice-ruby/test/fixtures/inbox.yml +206 -0
  31. data/vendor/gvoice-ruby/test/fixtures/inbox_fixture.html +2854 -0
  32. data/vendor/gvoice-ruby/test/fixtures/login_fixture.html +567 -0
  33. data/vendor/gvoice-ruby/test/gvoice-ruby_test.rb +9 -0
  34. data/vendor/gvoice-ruby/test/inbox_parser_test.rb +54 -0
  35. data/vendor/gvoice-ruby/test/test_helper.rb +19 -0
  36. metadata +31 -3
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby -W1
2
+ # coding: UTF-8
3
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'gvoice-ruby'
5
+ include GvoiceRuby
6
+
7
+ voicebox = GvoiceRuby::Client.new
8
+
9
+ txt = {:phone_number => "3088721257", :text => "Testing sms from gvoice-ruby!" }
10
+
11
+ puts "Message sent!" if voicebox.send_sms(txt)
12
+
@@ -0,0 +1,10 @@
1
+ ---
2
+ :google_auth_url: https://www.google.com/accounts/ServiceLoginAuth
3
+ :google_account_password: your_google_account_password
4
+ :continue_url: https://www.google.com/voice
5
+ :last_message_start_time: 0
6
+ :bot_name: your_gtalk_or_jabber_bot_name
7
+ :google_service: grandcentral
8
+ :bot_password: your_gtalk_bot_password
9
+ :google_voice_feed_url: https://www.google.com/voice/inbox/recent
10
+ :google_account_email: your_google_email_address@gmail.com
@@ -0,0 +1,6 @@
1
+ # coding: UTF-8
2
+
3
+ $:.unshift(File.dirname(__FILE__))
4
+ require 'yaml'
5
+ require 'gvoice-ruby/client'
6
+ require 'gvoice-ruby/config'
@@ -0,0 +1,9 @@
1
+ # coding: utf-8
2
+ module GvoiceRuby
3
+ class Call < Struct.new(:id, :start_time, :display_number, :display_start_date_time, :display_start_time, :relative_start_time, :is_read, :starred, :labels, :from, :to)
4
+ #attr_accessor :id, :start_time, :display_number, :display_start_date_time, :display_start_time, :relative_start_time, :is_read, :labels, :from, :to, :text
5
+ #
6
+ #def initialize
7
+ #end
8
+ end
9
+ end
@@ -0,0 +1,264 @@
1
+ # coding: UTF-8
2
+ $:.unshift(File.dirname(__FILE__))
3
+ %w[curb nokogiri json sms voicemail call user logger compatibility inbox_parser open-uri].each { |lib| require lib }
4
+
5
+ module GvoiceRuby
6
+ class Client
7
+ include Curl
8
+
9
+ attr_accessor :unread_counts, :smss, :voicemails, :user, :all_messages, :calls
10
+ attr_reader :logger
11
+
12
+ def initialize(config = GvoiceRuby::Configurator.load_config)
13
+ if config[:google_account_email].nil? || config[:google_account_password].nil?
14
+ raise ArgumentError, "Invalid Google Account username or password provided."
15
+ else
16
+ @logger = Logger.new(config.has_key?(:logfile) ? config[:logfile] : STDERR)
17
+ @user = User.new(config[:google_account_email], config[:google_account_password])
18
+ @any_unread = []
19
+ @unread_counts = {}
20
+ @all_messages = []
21
+ initialize_curb
22
+ end
23
+
24
+ login(config)
25
+ set_rnr_se_token
26
+ end
27
+
28
+ def any_unread?
29
+ @any_unread
30
+ end
31
+
32
+ def logged_in?
33
+ !@curb_instance.nil?
34
+ end
35
+
36
+ def check(parser = GvoiceRuby::InboxParser.new)
37
+ inbox = parser.parse_page(fetch_page)
38
+
39
+ get_unread_counts(inbox)
40
+ @smss = parser.parse_sms_messages(inbox['messages'])
41
+ @voicemails = parser.parse_voicemail_messages(inbox['messages'])
42
+ @all_messages = smss | voicemails
43
+ @all_messages.sort_by!(&:start_time)
44
+ end
45
+
46
+ def missed(parser = GvoiceRuby::InboxParser.new)
47
+ inbox = parser.parse_page(fetch_page('https://www.google.com/voice/inbox/recent/missed/'))
48
+ parser.parse_calls(inbox['messages'])
49
+ end
50
+
51
+ def received(parser = GvoiceRuby::InboxParser.new)
52
+ inbox = parser.parse_page(fetch_page('https://www.google.com/voice/inbox/recent/received/'))
53
+ parser.parse_calls(inbox['messages'])
54
+ end
55
+
56
+ def placed(parser = GvoiceRuby::InboxParser.new)
57
+ inbox = parser.parse_page(fetch_page('https://www.google.com/voice/inbox/recent/placed/'))
58
+ parser.parse_calls(inbox['messages'])
59
+ end
60
+
61
+ def send_sms(options)
62
+ fields = [ PostField.content('phoneNumber', options[:phone_number]),
63
+ PostField.content('text', options[:text]),
64
+ PostField.content('_rnr_se', @_rnr_se) ]
65
+
66
+ options.merge!({ :post_url => "https://www.google.com/voice/sms/send" })
67
+
68
+ post(options, fields)
69
+ end
70
+
71
+ def call(options)
72
+ fields = [ PostField.content('outgoingNumber', options[:outgoing_number]),
73
+ PostField.content('forwardingNumber', options[:forwarding_number]),
74
+ PostField.content('phoneType', options[:phone_type] || 2),
75
+ PostField.content('subscriberNumber', 'undefined'),
76
+ PostField.content('remember', 0),
77
+ PostField.content('_rnr_se', @_rnr_se) ]
78
+
79
+ options.merge!({ :post_url => "https://www.google.com/voice/call/connect" })
80
+
81
+ post(options, fields)
82
+ end
83
+
84
+ def cancel_call(options = {})
85
+ fields = [ PostField.content('outgoingNumber', options[:outgoing_number] || 'undefined'),
86
+ PostField.content('forwardingNumber', options[:forwarding_number] || 'undefined'),
87
+ PostField.content('cancelType', 'C2C'),
88
+ PostField.content('_rnr_se', @_rnr_se) ]
89
+
90
+ options.merge!({ :post_url => "https://www.google.com/voice/call/cancel" })
91
+
92
+ post(options, fields)
93
+ end
94
+
95
+ def archive(options)
96
+ fields = [ PostField.content('messages', options[:id]),
97
+ PostField.content('archive', 1),
98
+ PostField.content('_rnr_se', @_rnr_se) ]
99
+
100
+ options.merge!({ :post_url => 'https://www.google.com/voice/inbox/mark'})
101
+
102
+ post(options, fields)
103
+ end
104
+
105
+ def mark_as_read(options)
106
+ fields = [ PostField.content('messages', options[:id]),
107
+ PostField.content('read', 1),
108
+ PostField.content('_rnr_se', @_rnr_se) ]
109
+
110
+ options.merge!({ :post_url => 'https://www.google.com/voice/inbox/mark'})
111
+
112
+ post(options, fields)
113
+ end
114
+
115
+ def mark_as_unread(options)
116
+ fields = [ PostField.content('messages', options[:id]),
117
+ PostField.content('read', 0),
118
+ PostField.content('_rnr_se', @_rnr_se) ]
119
+
120
+ options.merge!({ :post_url => 'https://www.google.com/voice/inbox/mark'})
121
+
122
+ post(options, fields)
123
+ end
124
+
125
+ def star(options)
126
+ fields = [ PostField.content('messages', options[:id]),
127
+ PostField.content('star', 1),
128
+ PostField.content('_rnr_se', @_rnr_se) ]
129
+
130
+ options.merge!({ :post_url => 'https://www.google.com/voice/inbox/star'})
131
+
132
+ post(options, fields)
133
+ end
134
+
135
+ def unstar(options)
136
+ fields = [ PostField.content('messages', options[:id]),
137
+ PostField.content('star', 0),
138
+ PostField.content('_rnr_se', @_rnr_se) ]
139
+
140
+ options.merge!({ :post_url => 'https://www.google.com/voice/inbox/star'})
141
+
142
+ post(options, fields)
143
+ end
144
+
145
+ def add_note(options)
146
+ fields = [ PostField.content('id', options[:id]),
147
+ PostField.content('note', options[:note]),
148
+ PostField.content('_rnr_se', @_rnr_se) ]
149
+
150
+ options.merge!({ :post_url => 'https://www.google.com/voice/inbox/savenote'})
151
+
152
+ post(options, fields)
153
+ end
154
+
155
+ def delete_note(options)
156
+ fields = [ PostField.content('id', options[:id]),
157
+ PostField.content('_rnr_se', @_rnr_se) ]
158
+
159
+ options.merge!({ :post_url => 'https://www.google.com/voice/inbox/deletenote'})
160
+
161
+ post(options, fields)
162
+ end
163
+
164
+ def logout
165
+ if logged_in?
166
+ @curb_instance.url = "https://www.google.com/voice/account/signout"
167
+ @curb_instance.perform
168
+ logger.info logger.info "FINISHED LOGOUT #{@curb_instance.url}: HTTP #{@curb_instance.response_code}"
169
+ @curb_instance = nil
170
+ end
171
+ self
172
+ end
173
+
174
+ private
175
+ def login(options = {})
176
+ @curb_instance.url = 'https://www.google.com/accounts/ServiceLoginAuth'
177
+ @curb_instance.perform
178
+
179
+ defeat_google_xsrf(@curb_instance.body_str)
180
+
181
+ fields = [ PostField.content('continue', options[:continue_url]), #'https://www.google.com/voice'
182
+ PostField.content('GALX', @galx),
183
+ PostField.content('service', options[:google_service]),
184
+ PostField.content('Email', options[:google_account_email]),
185
+ PostField.content('Passwd', options[:google_account_password]) ]
186
+
187
+ options.merge!({ :post_url => 'https://www.google.com/accounts/ServiceLoginAuth' })
188
+
189
+ post(options, fields)
190
+ end
191
+
192
+ def post(options, fields)
193
+ @curb_instance.url = options[:post_url] #"https://www.google.com/voice/call/connect || https://www.google.com/voice/sms/send"
194
+ @curb_instance.http_post(fields)
195
+
196
+ logger.info "FINISHED POST TO #{options[:post_url]}: HTTP #{@curb_instance.response_code}"
197
+ return @curb_instance
198
+ end
199
+
200
+ def fetch_page(url = 'https://www.google.com/voice/inbox/recent')
201
+
202
+ @curb_instance.url = url #"https://www.google.com/voice/inbox/recent"
203
+
204
+ @curb_instance.http_get
205
+
206
+ logger.info "FINISHED FETCHING #{url}: HTTP #{@curb_instance.response_code}"
207
+ return @curb_instance
208
+ end
209
+
210
+ def get_unread_counts(inbox)
211
+ @unread_counts = inbox['unreadCounts']
212
+ @any_unread = inbox['unreadCounts']['unread'].to_i != 0
213
+ logger.info "No unread messages in your Google Voice inbox." unless @any_unread
214
+ end
215
+
216
+ def defeat_google_xsrf(body_string)
217
+ # defeat Google's XSRF protection
218
+ doc = Nokogiri::HTML::DocumentFragment.parse(body_string)
219
+ doc.css('div.loginBox table#gaia_table input').each do |input|
220
+ if input.to_s =~ /GALX/
221
+ @galx = input.to_s.scan(/value\="(.+?)"/).flatten!.pop
222
+ # p @galx
223
+ else
224
+ next
225
+ # raise IOError, 'Cannot fetch galx attribute from Google.'
226
+ end
227
+ end
228
+ end
229
+
230
+ def set_rnr_se_token
231
+ if @curb_instance.response_code == 200 #&& @curb_instance.respond_to?(:perform) # Vestigial?
232
+ @curb_instance.url = "http://www.google.com/voice"
233
+ @curb_instance.perform
234
+ @_rnr_se = extract_rnr_se(@curb_instance.body_str)
235
+ else
236
+ raise IOError, "Curb instance was not properly initialized."
237
+ end
238
+ end
239
+
240
+ def extract_rnr_se(body_string)
241
+ begin
242
+ /value="(.+)"/.match(Nokogiri::HTML::Document.parse(body_string).css('form#gc-search-form').inner_html)
243
+ return $1
244
+ rescue IOError
245
+ raise IOError, "Problem extracting _rnr_se code from page."
246
+ end
247
+ end
248
+
249
+ def initialize_curb
250
+ @curb_instance = Easy.new do |curl|
251
+ # Google gets mad if you don't fake this...
252
+ curl.headers["User-Agent"] = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2"
253
+ # Let's see what happens under the hood
254
+ # curl.verbose = true
255
+
256
+ # Google will redirect us a bit
257
+ curl.follow_location = true
258
+
259
+ # Google will make sure we retain cookies
260
+ curl.enable_cookies = true
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+
3
+ if RUBY_VERSION < '1.9'
4
+ class Symbol
5
+ def to_proc
6
+ proc { |obj, *args| obj.send(self, *args) }
7
+ end
8
+ end
9
+
10
+ class Array
11
+ def sort_by!(&given_proc)
12
+ if block_given?
13
+ self.sort! { |a,b| given_proc.call(a) <=> given_proc.call(b) }
14
+ else
15
+ raise ArgumentError "No valid proc object created from argument."
16
+ end
17
+ end
18
+ end
19
+ end
20
+ #
21
+ # class Thing
22
+ # def initialize
23
+ # @foo = rand(100)
24
+ # end
25
+ #
26
+ # attr_accessor :foo
27
+ # end
28
+ #
29
+ # a = []
30
+ #
31
+ # 10.times do
32
+ # a << Thing.new
33
+ # end
34
+ #
35
+ # a.sort_by!(&:foo)
36
+ #
37
+ # p a
@@ -0,0 +1,39 @@
1
+ # coding: UTF-8
2
+ $:.unshift(File.dirname(__FILE__))
3
+
4
+ module GvoiceRuby
5
+ class Configurator
6
+ PROJECT_ROOT = File.expand_path(File.dirname(__FILE__) + '/../..')
7
+
8
+ def self.load_config(config_file = File.join(PROJECT_ROOT, 'config', 'gvoice-ruby-config.yml'))
9
+ # Load our config
10
+ begin
11
+ if File.exists?(config_file)
12
+ config_hash = File.open(config_file) { |yf| YAML::load(yf) }
13
+ else
14
+ raise IOError
15
+ end
16
+ rescue IOError
17
+ STDERR.puts "Failed to open file #{File.expand_path(config_file)} for reading. File doesn't seem to exist. (#{$!})"
18
+ raise
19
+ end
20
+ return config_hash
21
+ end
22
+
23
+ def self.write_config(config_hash, config_file = File.join(PROJECT_ROOT, 'config', 'gvoice-ruby-config.yml'))
24
+ # Clean things up and put them away
25
+ begin
26
+ if File.exists?(config_file)
27
+ File.open(config_file, 'w' ) do |out_file|
28
+ YAML.dump(config_hash, out_file)
29
+ end
30
+ else
31
+ raise IOError
32
+ end
33
+ rescue IOError
34
+ STDERR.puts "Failed to open #{File.expand_path(config_file)} for writing. File doesn't seem to exist: (#{$!})"
35
+ raise
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,126 @@
1
+ # coding: utf-8
2
+ module GvoiceRuby
3
+ class InboxParser
4
+
5
+ def initialize
6
+ @smss = []
7
+ @voicemails = []
8
+ @calls = []
9
+ end
10
+
11
+ def parse_page(page_obj)
12
+ doc = Nokogiri::XML.parse(page_obj.body_str)
13
+
14
+ # p doc
15
+
16
+ @html_fragment = Nokogiri::HTML::DocumentFragment.parse(doc.to_html)
17
+
18
+ # p @html_fragment
19
+
20
+ m = doc.css('json').first.to_s.scan(/CDATA\[(.+)\]\]/).flatten
21
+
22
+ inbox = JSON.parse(m.first)
23
+ return inbox
24
+ end
25
+
26
+ def parse_sms_messages(messages, page_fragment = @html_fragment)
27
+ messages.each do |txt|
28
+ if txt[1]['type'].to_i == 2
29
+ next
30
+ else
31
+ txt_obj = Sms.new
32
+ txt_obj.id = txt[0]
33
+ txt_obj.start_time = txt[1]['startTime'].to_i
34
+ txt_obj.is_read = txt[1]['isRead']
35
+ txt_obj.display_start_time = txt[1]['displayStartTime']
36
+ txt_obj.relative_start_time = txt[1]['relativeStartTime']
37
+ txt_obj.display_number = txt[1]['displayNumber']
38
+ txt_obj.display_start_date_time = txt[1]['displayStartDateTime']
39
+ txt_obj.labels = txt[1]['labels']
40
+ @smss << txt_obj
41
+ @smss.sort_by!(&:start_time) #if @smss.respond_to?(:sort_by!)
42
+ end
43
+ end
44
+
45
+ @smss.each do |txt_obj|
46
+ page_fragment.css("div.gc-message-sms-row").each do |row|
47
+ if row.css('span.gc-message-sms-from').inner_html.strip! =~ /Me:/
48
+ next
49
+ elsif row.css('span.gc-message-sms-time').inner_html =~ Regexp.new(txt_obj.display_start_time)
50
+ txt_obj.to = 'Me'
51
+ txt_obj.from = row.css('span.gc-message-sms-from').inner_html.strip!.gsub!(':', '')
52
+ txt_obj.text = row.css('span.gc-message-sms-text').inner_html
53
+ # txt_obj.time = row.css('span.gc-message-sms-time').inner_html
54
+ else
55
+ next
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def parse_voicemail_messages(messages, page_fragment = @html_fragment)
62
+ # p messages
63
+ messages.each do |msg|
64
+ if msg[1]['type'].to_i == 2
65
+ vm_obj = Voicemail.new
66
+ vm_obj.id = msg[0]
67
+ vm_obj.start_time = msg[1]['startTime'].to_i
68
+ vm_obj.is_read = msg[1]['isRead']
69
+ vm_obj.display_start_time = msg[1]['displayStartTime']
70
+ vm_obj.relative_start_time = msg[1]['relativeStartTime']
71
+ vm_obj.display_number = msg[1]['displayNumber']
72
+ vm_obj.display_start_date_time = msg[1]['displayStartDateTime']
73
+ vm_obj.labels = msg[1]['labels']
74
+ @voicemails << vm_obj
75
+ @voicemails.sort_by!(&:start_time)
76
+ else
77
+ next
78
+ end
79
+ end
80
+
81
+ @voicemails.each do |vm_obj|
82
+ page_fragment.css('table.gc-message-tbl').each do |row|
83
+ if row.css('span.gc-message-time').text =~ Regexp.new(vm_obj.display_start_date_time)
84
+ vm_obj.to = 'Me'
85
+ vm_obj.from = row.css('a.gc-under.gc-message-name-link').inner_html
86
+ vm_obj.transcript = row.css('div.gc-message-message-display').inner_text.to_s.gsub(/\n/, "").squeeze(" ").strip!
87
+ # vm_obj.time = row.css('span.gc-message-time').inner_html
88
+ else
89
+ next
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ def parse_calls(messages, page_fragment = @html_fragment)
96
+ messages.each do |msg|
97
+ call_obj = Call.new
98
+ call_obj.id = msg[0]
99
+ call_obj.start_time = msg[1]['startTime'].to_i
100
+ call_obj.is_read = msg[1]['isRead']
101
+ call_obj.display_start_time = msg[1]['displayStartTime']
102
+ call_obj.relative_start_time = msg[1]['relativeStartTime']
103
+ call_obj.display_number = msg[1]['displayNumber']
104
+ call_obj.display_start_date_time = msg[1]['displayStartDateTime']
105
+ call_obj.labels = msg[1]['labels']
106
+
107
+ @calls << call_obj
108
+ @calls.sort_by!(&:start_time)
109
+ end
110
+
111
+ @calls.each do |call_obj|
112
+ page_fragment.css('table.gc-message-tbl').each do |row|
113
+ if row.css('span.gc-message-time').text =~ Regexp.new(call_obj.display_start_date_time)
114
+ call_obj.to = 'Me'
115
+ call_obj.from = call_obj.display_number
116
+ # call_obj.from = row.css('a.gc-under.gc-message-name-link').inner_html
117
+ # call_obj.transcript = row.css('div.gc-message-message-display').inner_text.to_s.gsub(/\n/, "").squeeze(" ").strip!
118
+ # call_obj.time = row.css('span.gc-message-time').inner_html
119
+ else
120
+ next
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end