harleytt-gvoice-ruby 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,75 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'pathname'
3
+
4
+ # Pathname#ancestor takes an integer argument and walks up the path the same number of steps as the integer argument (starting from zero).
5
+ # Hence, given the path /Users/foo/bar/baz/bat/quux, Pathname#ancestor(3) would return /Users/foo/bar.
6
+ Pathname.class_eval do
7
+ def ancestor(num)
8
+ temp = self
9
+ num.downto(1) do
10
+ temp = temp.parent
11
+ end
12
+ return temp
13
+ end unless method_defined?(:ancestor)
14
+ end
15
+
16
+ if RUBY_VERSION > '1.9'
17
+ module GvoiceRuby
18
+ class Call
19
+ alias_method :orig_display_start_date_time, :display_start_date_time
20
+
21
+ def display_start_date_time # New Date class in Ruby 1.9.2 only accepts strictly formatted strings Date.parse
22
+ if self.send(:caller).first.include?('inbox_parser')
23
+ orig_display_start_date_time
24
+ else
25
+ # Capture the original date string and parse into month, day, year variables
26
+ # warn "#{self.send(:caller)}"
27
+
28
+ orig_display_start_date_time.match(/^(\d)\/(\d{1,2})\/(\d{2})\s(.+)\z/)
29
+ # month = $1
30
+ # day = $2
31
+ year = "20" + $3
32
+ # warn "Month is: #{month}\nDay is: #{day}\nYear is: #{year}"
33
+ "#{year}-#{$1}-#{$2} #{$4}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ if RUBY_VERSION < '1.9'
41
+
42
+ class Symbol
43
+ def to_proc
44
+ proc { |obj, *args| obj.send(self, *args) }
45
+ end
46
+ end
47
+
48
+ class Array
49
+ def sort_by!(&given_proc)
50
+ if block_given?
51
+ self.sort! { |a,b| given_proc.call(a) <=> given_proc.call(b) }
52
+ else
53
+ raise ArgumentError "No valid proc object created from argument."
54
+ end
55
+ end
56
+ end
57
+ end
58
+ #
59
+ # class Thing
60
+ # def initialize
61
+ # @foo = rand(100)
62
+ # end
63
+ #
64
+ # attr_accessor :foo
65
+ # end
66
+ #
67
+ # a = []
68
+ #
69
+ # 10.times do
70
+ # a << Thing.new
71
+ # end
72
+ #
73
+ # a.sort_by!(&:foo)
74
+ #
75
+ # p a
@@ -0,0 +1,39 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # $:.unshift(File.dirname(__FILE__))
3
+
4
+ module GvoiceRuby
5
+ class Configurator
6
+ PROJECT_ROOT = File.expand_path(Pathname.new(__FILE__).ancestor(3))
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,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module GvoiceRuby
3
+ class LoginFailed < StandardError
4
+ end
5
+ end
@@ -0,0 +1,126 @@
1
+ # -*- encoding: 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
@@ -0,0 +1,9 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module GvoiceRuby
3
+ class Sms < Struct.new(:id, :start_time, :display_number, :display_start_date_time, :display_start_time, :relative_start_time, :is_read, :starred, :labels, :from, :to, :text)
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,12 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module GvoiceRuby
3
+ class User
4
+ # User is not a struct because we require email and password attributes
5
+ attr_accessor :email, :password
6
+
7
+ def initialize(email, password)
8
+ @email = email
9
+ @password = password
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module GvoiceRuby
3
+ VERSION = "0.3.3"
4
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module GvoiceRuby
3
+ class Voicemail < Struct.new(:id, :start_time, :display_number, :display_start_date_time, :display_start_time, :relative_start_time, :is_read, :starred, :labels, :from, :to, :transcript, :file)
4
+ #attr_accessor :id, :start_time, :display_number, :display_start_date_time, :display_start_time, :relative_start_time, :is_read, :labels, :from, :to, :transcript, :file
5
+ #
6
+ #def initialize
7
+ #
8
+ #end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.unshift(File.dirname(__FILE__))
3
+ require 'yaml'
4
+ require 'gvoice-ruby/client'
5
+ require 'gvoice-ruby/config'
@@ -0,0 +1,59 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.unshift "." # Ruby 1.9.2 does not include current directory in the path
3
+ require File.dirname(__FILE__) + "/test_helper"
4
+ require 'mocha'
5
+
6
+ class ClientTest < Test::Unit::TestCase
7
+ def setup
8
+ @config_file = File.join(File.dirname(__FILE__), 'fixtures', 'config_fixture.yml')
9
+ @page_body = String.new(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'login_fixture.html')))
10
+ end
11
+
12
+ should "raise argument error if username nil" do
13
+ assert_raise(ArgumentError) { GvoiceRuby::Client.new({ :google_account_email => nil }) }
14
+ end
15
+
16
+ should "raise argument error if password nil" do
17
+ assert_raise(ArgumentError) { GvoiceRuby::Client.new({ :google_account_password => nil }) }
18
+ end
19
+
20
+ should "accept a simple hash as parameters" do
21
+ Curl::Easy.any_instance.stubs(:perform).returns(true)
22
+ Curl::Easy.any_instance.stubs(:body_str).returns(@page_body)
23
+ client = GvoiceRuby::Client.new({:google_account_email => 'google_test_account@gmail.com', :google_account_password => "bar"})
24
+ assert_kind_of(GvoiceRuby::Client, client)
25
+ # assert_equal(File.join(File.dirname(File.dirname(__FILE__)), 'log', 'gvoice-ruby.log'),
26
+ # client.logger.instance_variable_get(:@logdev).instance_variable_get(:@filename))
27
+ end
28
+
29
+ should "raise an error when unable to connect to Google" do
30
+ # Curl::Easy.any_instance.stubs(:perform).returns(false)
31
+ # Curl::Easy.any_instance.stubs(:response_code).returns()
32
+ client = GvoiceRuby::Client.new(GvoiceRuby::Configurator.load_config(@config_file))
33
+ assert_equal(client.instance_variable_get(:@curb_instance).response_code, 405)
34
+ end
35
+
36
+ should "raise an error when failing to login" do
37
+ Curl::Easy.any_instance.stubs(:perform).returns(false)
38
+ assert_raise(GvoiceRuby::LoginFailed) do
39
+ GvoiceRuby::Client.new({:google_account_email => 'google_test_account@gmail.com', :google_account_password => "bar"})
40
+ end
41
+ end
42
+
43
+ should "login" do
44
+ Curl::Easy.any_instance.stubs(:body_str).returns(@page_body)
45
+ client = GvoiceRuby::Client.new(GvoiceRuby::Configurator.load_config(@config_file))
46
+ assert client.logged_in?
47
+ assert_kind_of(Curl::Easy, client.instance_variable_get(:@curb_instance))
48
+ end
49
+
50
+ should "logout" do
51
+ Curl::Easy.any_instance.stubs(:body_str).returns(@page_body)
52
+ client = GvoiceRuby::Client.new(GvoiceRuby::Configurator.load_config(@config_file))
53
+ assert client.logged_in?
54
+ assert_kind_of(Curl::Easy, client.instance_variable_get(:@curb_instance))
55
+ Curl::Easy.any_instance.stubs(:perform).returns(true)
56
+ client.logout
57
+ deny client.logged_in?
58
+ end
59
+ end
@@ -0,0 +1,47 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.unshift "." # Ruby 1.9.2 does not include current directory in the path
3
+ require File.dirname(__FILE__) + "/test_helper"
4
+ require 'mocha'
5
+
6
+ class CompatibilityTest < Test::Unit::TestCase
7
+
8
+ def setup
9
+ @page_body = String.new(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'inbox_fixture.html')))
10
+ @inbox_fixture = File.open(File.join(File.dirname(__FILE__), 'fixtures', 'inbox.yml')) { |yf| YAML::load(yf) }
11
+ @page_obj = mock()
12
+ @page_obj.stubs(:body_str).returns(@page_body)
13
+ end
14
+
15
+ should "add an instance method to Pathname" do
16
+ assert Pathname.new("/blah").respond_to?(:ancestor)
17
+ end
18
+
19
+ should "return the appropriate ancestor path" do
20
+ path = Pathname.new("/Users/foo/bar/baz/bat/quux")
21
+ assert_equal(Pathname.new("/Users/foo/bar"), path.ancestor(3))
22
+ end
23
+
24
+ if RUBY_VERSION > '1.9'
25
+ context "Using Ruby 1.9" do
26
+ should "Provide correct format of the display_start_date_time method" do
27
+ GvoiceRuby::Client.any_instance.stubs(:fetch_page).returns(true)
28
+ parser = GvoiceRuby::InboxParser.new
29
+ inbox = parser.parse_page(@page_obj)
30
+ parser.parse_calls(inbox['messages'])
31
+ assert_equal(parser.instance_variable_get(:@calls)[0].display_start_date_time, "2010-2-17 11:36 AM")
32
+ assert_not_equal(parser.instance_variable_get(:@calls)[0].display_start_date_time, "2/17/2010 11:36 AM")
33
+ end
34
+ end
35
+ else
36
+ context "Using Ruby 1.8" do
37
+ should "Provide correct format of the display_start_date_time method" do
38
+ GvoiceRuby::Client.any_instance.stubs(:fetch_page).returns(true)
39
+ parser = GvoiceRuby::InboxParser.new
40
+ inbox = parser.parse_page(@page_obj)
41
+ parser.parse_calls(inbox['messages'])
42
+ assert_not_equal(parser.instance_variable_get(:@calls)[0].display_start_date_time, "2010-2-17 11:36 AM")
43
+ assert_equal(parser.instance_variable_get(:@calls)[0].display_start_date_time, "2/17/10 11:36 AM")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.dirname(__FILE__) + "/test_helper"
3
+
4
+ class ConfigTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @config_file = File.join(File.dirname(__FILE__), 'fixtures', 'config_fixture.yml')
8
+ @config = GvoiceRuby::Configurator.load_config(@config_file)
9
+ end
10
+
11
+ should "have project root constant" do
12
+ assert_equal(File.expand_path(File.dirname(__FILE__) + '/..'), GvoiceRuby::Configurator.const_get(:PROJECT_ROOT).to_s)
13
+ end
14
+
15
+ should "load configuration file correctly" do
16
+ @config.each_pair do |k,v|
17
+ assert_equal(v, @config[k.to_sym])
18
+ end
19
+ end
20
+
21
+ should "write configuration file" do
22
+ @config[:foo] = 'bar'
23
+ GvoiceRuby::Configurator.write_config(@config, File.dirname(__FILE__) + '/fixtures/config_fixture.yml')
24
+ newly_loaded_config = GvoiceRuby::Configurator.load_config(File.dirname(__FILE__) + '/fixtures/config_fixture.yml')
25
+ assert_equal('bar', newly_loaded_config[:foo].to_s)
26
+ @config.delete(:foo)
27
+ GvoiceRuby::Configurator.write_config(@config, File.dirname(__FILE__) + '/fixtures/config_fixture.yml')
28
+ end
29
+
30
+ should "raise IOError when config file not loaded" do
31
+ begin
32
+ assert_raise(IOError) { GvoiceRuby::Configurator.load_config('foo') }
33
+ rescue StandardError
34
+ end
35
+ end
36
+
37
+ should "raise IOError when config file not written" do
38
+ begin
39
+ assert_raise(IOError) { GvoiceRuby::Configurator.write_config(@config, 'foo') }
40
+ rescue StandardError
41
+ end
42
+ end
43
+
44
+ should "Load a logger" do
45
+ assert_equal(@config[:logfile], './log/test_log.log')
46
+ assert_not_nil(GvoiceRuby::Client.new(@config).logger)
47
+ assert_not_nil(File.read('./log/test_log.log'))
48
+ assert_match(/^# Log/, File.read('./log/test_log.log'))
49
+ end
50
+ end
@@ -0,0 +1,11 @@
1
+ ---
2
+ :bot_password: foo
3
+ :google_service: grandcentral
4
+ :last_message_start_time: 1252936650000
5
+ :google_voice_feed_url: https://www.google.com/voice/inbox/recent
6
+ :google_account_email: google_test_account@gmail.com
7
+ :bot_name: test_case@jabber.org
8
+ :google_account_password: bar
9
+ :logfile: ./log/test_log.log
10
+ :continue_url: https://www.google.com/voice
11
+ :google_auth_url: https://www.google.com/accounts/ServiceLoginAuth
@@ -0,0 +1 @@
1
+ "{\"messages\":{\"9f051f4869a4aab44b00962187680eb558408343\":{\"id\":\"9f051f4869a4aab44b00962187680eb558408343\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252546280464\",\"displayStartDateTime\":\"9/9/09 8:31 PM\",\"displayStartTime\":\"8:31 PM\",\"relativeStartTime\":\"40 hours ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":10,\"children\":\"\"},\"1c09172ec9043a80b5fe30b6310d84faa8aa1b12\":{\"id\":\"1c09172ec9043a80b5fe30b6310d84faa8aa1b12\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252525590500\",\"displayStartDateTime\":\"9/9/09 2:46 PM\",\"displayStartTime\":\"2:46 PM\",\"relativeStartTime\":\"46 hours ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":10,\"children\":\"\"},\"0cc0fa9a10c91187a182c76a10519185747a05be\":{\"id\":\"0cc0fa9a10c91187a182c76a10519185747a05be\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252517413901\",\"displayStartDateTime\":\"9/9/09 12:30 PM\",\"displayStartTime\":\"12:30 PM\",\"relativeStartTime\":\"2 days ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":10,\"children\":\"\"},\"d8a7642677249d3b35fb029fd806730fe77fa309\":{\"id\":\"d8a7642677249d3b35fb029fd806730fe77fa309\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252449746310\",\"displayStartDateTime\":\"9/8/09 5:42 PM\",\"displayStartTime\":\"5:42 PM\",\"relativeStartTime\":\"2 days ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":10,\"children\":\"\"},\"5ccf9f66a6c3ae9fe05b716d8811a14d2c505a2f\":{\"id\":\"5ccf9f66a6c3ae9fe05b716d8811a14d2c505a2f\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252443634536\",\"displayStartDateTime\":\"9/8/09 4:00 PM\",\"displayStartTime\":\"4:00 PM\",\"relativeStartTime\":\"2 days ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":10,\"children\":\"\"},\"8b710ab3b55926fd5171b0e0394350ec4a38b5ac\":{\"id\":\"8b710ab3b55926fd5171b0e0394350ec4a38b5ac\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252374029866\",\"displayStartDateTime\":\"9/7/09 8:40 PM\",\"displayStartTime\":\"8:40 PM\",\"relativeStartTime\":\"3 days ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":10,\"children\":\"\"},\"38fb0f098daa670ec648b2e947f2a6e4dcfc58a6\":{\"id\":\"38fb0f098daa670ec648b2e947f2a6e4dcfc58a6\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252278164000\",\"displayStartDateTime\":\"9/6/09 6:02 PM\",\"displayStartTime\":\"6:02 PM\",\"relativeStartTime\":\"4 days ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"voicemail\",\"all\"],\"type\":2,\"children\":\"\"},\"833332d3f90330a4d929eb698b4240ad1f79780f\":{\"id\":\"833332d3f90330a4d929eb698b4240ad1f79780f\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252265039929\",\"displayStartDateTime\":\"9/6/09 2:23 PM\",\"displayStartTime\":\"2:23 PM\",\"relativeStartTime\":\"4 days ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":11,\"children\":\"\"},\"79d84552fecdba8160669e479d7897d368a1efb2\":{\"id\":\"79d84552fecdba8160669e479d7897d368a1efb2\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252210001428\",\"displayStartDateTime\":\"9/5/09 11:06 PM\",\"displayStartTime\":\"11:06 PM\",\"relativeStartTime\":\"5 days ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":10,\"children\":\"\"},\"23459dc9b09799115ff5b4c2f0c502a99e934025\":{\"id\":\"23459dc9b09799115ff5b4c2f0c502a99e934025\",\"phoneNumber\":\"+15555555555\",\"displayNumber\":\"(555) 555-5555\",\"startTime\":\"1252180801154\",\"displayStartDateTime\":\"9/5/09 3:00 PM\",\"displayStartTime\":\"3:00 PM\",\"relativeStartTime\":\"5 days ago\",\"note\":\"\",\"isRead\":true,\"isSpam\":false,\"isTrash\":false,\"star\":false,\"labels\":[\"inbox\",\"sms\",\"all\"],\"type\":10,\"children\":\"\"}},\"totalSize\":47,\"unreadCounts\":{\"all\":0,\"inbox\":0,\"missed\":0,\"placed\":0,\"received\":0,\"recorded\":0,\"sms\":0,\"trash\":0,\"unread\":0,\"voicemail\":0},\"resultsPerPage\":10}"