harleytt-gvoice-ruby 0.3.3

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,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}"