diff --git a/.gitignore b/.gitignore index 3cc9a5c..9ab2124 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ .DS_Store a.pl env -fixtures cover_db diff --git a/lib/Google/Voice.pm b/lib/Google/Voice.pm index 7db7969..613cd88 100644 --- a/lib/Google/Voice.pm +++ b/lib/Google/Voice.pm @@ -6,131 +6,125 @@ use warnings; use Mojo::Client; use Mojo::JSON; use IO::Socket::SSL; -use Data::Dumper; use Google::Voice::Feed; use Google::Voice::Call; use base 'Mojo::Base'; -__PACKAGE__->attr( [ qw/ client rnr_se / ] ); +__PACKAGE__->attr([qw/ client rnr_se /]); sub new { - my $self = bless {}, shift; + my $self = bless {}, shift; - $self->client( Mojo::Client->new ); + $self->client(Mojo::Client->new); - return $self; + return $self; } sub login { - my $self = shift; - my ($user, $pass) = @_; - my $c = $self->client; + my $self = shift; + my ($user, $pass) = @_; + my $c = $self->client; - # GALX value - my $el = $c->get('https://www.google.com/accounts/ServiceLogin') - ->res->dom->at('input[name="GALX"]'); - - my $galx = $el->attrs->{value} if $el; + # GALX value + my $el = + $c->get('https://www.google.com/accounts/ServiceLogin') + ->res->dom->at('input[name="GALX"]'); - $c->post_form('https://www.google.com/accounts/ServiceLoginAuth', { - Email => $user, - Passwd => $pass, - GALX => $galx, - } ); - - # rnr_se required for subsequent requests - $c->max_redirects(4); # 3-4 redirects before rnr_se is available - $el = $c->get('https://www.google.com/voice#inbox') - ->res->dom->at('input[name="_rnr_se"]'); - - # Login not accepted - return unless $el; - - $self->rnr_se( $el->attrs->{value} ); + my $galx = $el->attrs->{value} if $el; - return $self; -} + $c->post_form( + 'https://www.google.com/accounts/ServiceLoginAuth', + { Email => $user, + Passwd => $pass, + GALX => $galx, + } + ); -#sub logout { -# my $self = shift; -# my $c = $self->client; -# -# $c->max_redirects(0); -# -# return 1 if $c->get('https://www.google.com/voice/account/signout') -# ->res->cookie('gv')->value eq 'EXPIRED'; -#} + # rnr_se required for subsequent requests + $c->max_redirects(4); # 3-4 redirects before rnr_se is available + $el = + $c->get('https://www.google.com/voice#inbox') + ->res->dom->at('input[name="_rnr_se"]'); + + # Login not accepted + return unless $el; + + $self->rnr_se($el->attrs->{value}); + + return $self; +} sub send_sms { - my $self = shift; - my $c = $self->client; - my ($phone, $content) = @_; - - my $json = $c->post_form( - 'https://www.google.com/voice/b/0/sms/send', { - id => undef, - phoneNumber => $phone, - text => $content || '', - _rnr_se => $self->rnr_se - } - )->res->json; - - $@ = $json->{data}->{code} and return unless $json->{ok}; - - return $json->{ok}; + my $self = shift; + my $c = $self->client; + my ($phone, $content) = @_; + + my $json = $c->post_form( + 'https://www.google.com/voice/b/0/sms/send', + { id => undef, + phoneNumber => $phone, + text => $content || '', + _rnr_se => $self->rnr_se + } + )->res->json; + + $@ = $json->{data}->{code} and return unless $json->{ok}; + + return $json->{ok}; } -for my $feed ( qw/ all starred spam trash voicemail - sms recorded placed received missed / ) { - - no strict 'refs'; - *{"Google::Voice::${feed}"} = sub { - shift->feed( 'https://www.google.com/voice/inbox/recent/' . $feed ); - }; +for my $feed ( + qw/ all starred spam trash voicemail + sms recorded placed received missed / + ) +{ + + no strict 'refs'; + *{"Google::Voice::${feed}"} = sub { + shift->feed('https://www.google.com/voice/inbox/recent/' . $feed); + }; } sub feed { - my $self = shift; - my $url = shift; + my $self = shift; + my $url = shift; - my $c = $self->client; - - # Multiple conversations - my $inbox = $c->get( $url )->res->dom; + my $c = $self->client; - # metadata - my $meta = Mojo::JSON->new->decode( - $inbox->at('response > json')->text ); - - # content - my $xml = Mojo::DOM->new->parse( - $inbox->at('response > html')->text ); - - # Each conversation in a span.gc-message - return map - Google::Voice::Feed->new( $_, $meta, $self->rnr_se, $c ), - @{$xml->find('.gc-message')}; + # Multiple conversations + my $inbox = $c->get($url)->res->dom; + + # metadata + my $meta = Mojo::JSON->new->decode($inbox->at('response > json')->text); + + # content + my $xml = Mojo::DOM->new->parse($inbox->at('response > html')->text); + + # Each conversation in a span.gc-message + return map + Google::Voice::Feed->new($_, $meta, $self->rnr_se, $c), + @{$xml->find('.gc-message')}; } sub call { - my $self = shift; - my ($from, $to) = @_; - - my $json = $self->client->post_form( - 'https://www.google.com/voice/call/connect' => { - forwardingNumber => $from, - outgoingNumber => $to, - phoneType => 1, - remember => 0, - _rnr_se => $self->rnr_se - } - )->res->json; - - $@ = $json->{error} and return unless $json->{ok}; - - return Google::Voice::Call->new( @_, $self->rnr_se, $self->client ); + my $self = shift; + my ($from, $to) = @_; + + my $json = $self->client->post_form( + 'https://www.google.com/voice/call/connect' => { + forwardingNumber => $from, + outgoingNumber => $to, + phoneType => 1, + remember => 0, + _rnr_se => $self->rnr_se + } + )->res->json; + + $@ = $json->{error} and return unless $json->{ok}; + + return Google::Voice::Call->new(@_, $self->rnr_se, $self->client); } 1; diff --git a/lib/Google/Voice/Call.pm b/lib/Google/Voice/Call.pm index 4dacfd5..b514f87 100644 --- a/lib/Google/Voice/Call.pm +++ b/lib/Google/Voice/Call.pm @@ -5,35 +5,35 @@ use warnings; use base 'Mojo::Base'; -__PACKAGE__->attr( [ qw/ from to rnr_se client / ] ); +__PACKAGE__->attr([qw/ from to rnr_se client /]); sub new { - my $self = bless {}, shift; - - $self->from( shift ); - $self->to( shift ); - $self->rnr_se( shift ); - $self->client( shift ); - - return $self; + my $self = bless {}, shift; + + $self->from(shift); + $self->to(shift); + $self->rnr_se(shift); + $self->client(shift); + + return $self; } sub cancel { - my $self = shift; - my ($from, $to) = @_; + my $self = shift; + my ($from, $to) = @_; - my $json = $self->client->post_form( - 'https://www.google.com/voice/call/cancel/' => { - forwardingNumber => undef, - outgoingNumber => undef, - cancelType => 'C2C', - _rnr_se => $self->rnr_se - } - )->res->json; + my $json = $self->client->post_form( + 'https://www.google.com/voice/call/cancel/' => { + forwardingNumber => undef, + outgoingNumber => undef, + cancelType => 'C2C', + _rnr_se => $self->rnr_se + } + )->res->json; - $@ = $json->{data}->{code} and return unless $json->{ok}; + $@ = $json->{data}->{code} and return unless $json->{ok}; - return $json->{ok}; + return $json->{ok}; } 1; diff --git a/lib/Google/Voice/Feed.pm b/lib/Google/Voice/Feed.pm index 6624aac..11adb94 100644 --- a/lib/Google/Voice/Feed.pm +++ b/lib/Google/Voice/Feed.pm @@ -3,87 +3,85 @@ package Google::Voice::Feed; use strict; use warnings; -use Data::Dumper; use Google::Voice::SMS::Message; use base 'Mojo::Base'; use constant FEED_TYPE => { - 2 => 'voicemail', - 10 => 'sms', - 4 => 'recorded', - 13 => 'placed', - 1 => 'received', - 0 => 'missed', - 11 => 'trash', - 10 => 'starred', + 2 => 'voicemail', + 10 => 'sms', + 4 => 'recorded', + 13 => 'placed', + 1 => 'received', + 0 => 'missed', + 11 => 'trash', + 10 => 'starred', }; -__PACKAGE__->attr( [ qw/ xml id type name meta text rnr_se client / ] ); +__PACKAGE__->attr([qw/ xml id type name meta text rnr_se client /]); sub new { - my $self = bless {}, shift; - my $xml = shift; - my $meta = shift; - my $rnr_se = shift; - my $client = shift; - - $self->xml( $xml ); - $self->id( $xml->attrs->{id} ); - $self->name( $xml->at('.gc-message-name-link')->text ); - $self->meta( $meta->{messages}->{ $self->id } ); - $self->type( FEED_TYPE->{ $self->meta->{type} } ); - - $self->text( - "@{[map $_->text, @{$xml->find('.gc-message-message-display > span')}]}" - ); - - $self->rnr_se( $rnr_se ); - $self->client( $client ); - - return $self; + my $self = bless {}, shift; + my $xml = shift; + my $meta = shift; + my $rnr_se = shift; + my $client = shift; + + $self->xml($xml); + $self->id($xml->attrs->{id}); + $self->name($xml->at('.gc-message-name-link')->text); + $self->meta($meta->{messages}->{$self->id}); + $self->type(FEED_TYPE->{$self->meta->{type}}); + + $self->text( + "@{[map $_->text, @{$xml->find('.gc-message-message-display > span')}]}" + ); + + $self->rnr_se($rnr_se); + $self->client($client); + + return $self; } sub messages { - my $self = shift; - - # Each text message is a span.gc-message-sms-row - return map - Google::Voice::SMS::Message->new( - $_, $self->meta, $self->rnr_se, $self->client - ), - @{$self->xml->find('.gc-message-sms-row')}; + my $self = shift; + + # Each text message is a span.gc-message-sms-row + return + map Google::Voice::SMS::Message->new($_, $self->meta, $self->rnr_se, + $self->client), + @{$self->xml->find('.gc-message-sms-row')}; } sub latest { return (shift->messages)[-1] } sub delete { - my $self = shift; - - my $json = $self->client->post_form( - 'https://www.google.com/voice/inbox/deleteMessages' => { - messages => $self->id, - trash => 1, - _rnr_se => $self->rnr_se - } - )->res->json; - - $@ = $json->{data}->{code} and return unless $json->{ok}; - - return $json->{ok}; + my $self = shift; + + my $json = $self->client->post_form( + 'https://www.google.com/voice/inbox/deleteMessages' => { + messages => $self->id, + trash => 1, + _rnr_se => $self->rnr_se + } + )->res->json; + + $@ = $json->{data}->{code} and return unless $json->{ok}; + + return $json->{ok}; } sub download { - my $self = shift; - my ($from, $to) = @_; + my $self = shift; + my ($from, $to) = @_; - my $res = $self->client->get( - 'https://www.google.com/voice/media/send_voicemail/' . $self->id - )->res; + my $res = $self->client->get( + 'https://www.google.com/voice/media/send_voicemail/' . $self->id) + ->res; - $@ = $res->message and return if $res->code != 200; + $@ = $res->message and return if $res->code != 200; - return $res->content->asset; + return $res->content->asset; } 1; diff --git a/lib/Google/Voice/SMS/Message.pm b/lib/Google/Voice/SMS/Message.pm index fb62369..0d08f5c 100644 --- a/lib/Google/Voice/SMS/Message.pm +++ b/lib/Google/Voice/SMS/Message.pm @@ -3,34 +3,33 @@ package Google::Voice::SMS::Message; use strict; use warnings; -use Data::Dumper; use Mojo::ByteStream; use base 'Mojo::Base'; -__PACKAGE__->attr( [ qw/ text time inbound outbound xml rnr_se client / ] ); +__PACKAGE__->attr([qw/ text time inbound outbound xml rnr_se client /]); sub new { - my $self = bless {}, shift; - my $xml = shift; - my $meta = shift; - - $self->rnr_se( shift ); - $self->client( shift ); - - my $from = Mojo::ByteStream->new( - $xml->at('.gc-message-sms-from')->text )->trim; - - my $time = Mojo::ByteStream->new( - $xml->at('.gc-message-sms-time')->text )->trim; - - $self->xml( $xml ); - $self->text( $xml->at('.gc-message-sms-text')->text ); - $self->time( $time ); - $self->inbound( $from eq 'Me:' ); - $self->outbound( $from ne 'Me:' ); - - return $self; + my $self = bless {}, shift; + my $xml = shift; + my $meta = shift; + + $self->rnr_se(shift); + $self->client(shift); + + my $from = + Mojo::ByteStream->new($xml->at('.gc-message-sms-from')->text)->trim; + + my $time = + Mojo::ByteStream->new($xml->at('.gc-message-sms-time')->text)->trim; + + $self->xml($xml); + $self->text($xml->at('.gc-message-sms-text')->text); + $self->time($time); + $self->inbound($from eq 'Me:'); + $self->outbound($from ne 'Me:'); + + return $self; } 1; diff --git a/t/voice.t b/t/voice.t index 484ff1d..f101e0d 100644 --- a/t/voice.t +++ b/t/voice.t @@ -3,25 +3,35 @@ use warnings; use Test::More; use Test::Mojo; -use Data::Dumper; use Google::Voice; -my $name = $ENV{GVNAME}; -my @auth = ($ENV{GVUSER}, $ENV{GVPASS}); -my $phone = $ENV{GVPHONE}; +plan skip_all => 'Set TEST_ONLINE environment variable to enable tests. ' + . 'Requires an active Google Voice account.' + unless $ENV{TEST_ONLINE}; -my $vm_name = $ENV{EXISTING_VOICEMAIL_NAME}; -my $vm_phone = $ENV{EXISTING_VOICEMAIL_PHONE}; +plan tests => 50; -my $rec_name = $ENV{EXISTING_RECORDED_NAME}; -my $rec_phone = $ENV{EXISTING_RECORDED_PHONE}; +# Full name on account +my $name = $ENV{GVNAME}; +my @auth = ($ENV{GVUSER}, $ENV{GVPASS}); +my $phone = $ENV{GVPHONE}; # format: +15555555555 + +# Lastest voicemail +my $vm_name = $ENV{EXISTING_VOICEMAIL_NAME}; +my $vm_phone = $ENV{EXISTING_VOICEMAIL_PHONE}; # format: +15555555555 + +# Latest recording +my $rec_name = $ENV{EXISTING_RECORDED_NAME}; +my $rec_phone = $ENV{EXISTING_RECORDED_PHONE}; # format: +15555555555 + +# Real phone numbers to place sample call - real phones may ring +my $from_phone = $ENV{FROM_PHONE}; # format: +15555555555 +my $to_phone = $ENV{TO_PHONE}; # format: +15555555555 -my $from_phone = $ENV{FROM_PHONE}; -my $to_phone = $ENV{TO_PHONE}; # Login -ok ! Google::Voice->new->login, 'no auth'; -ok my $g = Google::Voice->new->login( @auth ), 'correct auth'; +ok !Google::Voice->new->login, 'no auth'; +ok my $g = Google::Voice->new->login(@auth), 'correct auth'; # voicemail inbox ok my $vm = ($g->voicemail)[0], 'voicemail inbox'; @@ -43,9 +53,9 @@ ok $asset = $node->download, 'recording'; ok $asset->size, $asset->size . ' bytes'; # sms -ok ! $g->send_sms( 'invalid #' => 'message' ), 'sms fail'; +ok !$g->send_sms('invalid #' => 'message'), 'sms fail'; is $@, 20, 'error'; -ok $g->send_sms( $phone => 'A' ), 'sms'; +ok $g->send_sms($phone => 'A'), 'sms'; # sms inbox ok my $conv = ($g->sms)[0], 'sms inbox'; @@ -58,12 +68,12 @@ my @m = $conv->messages; cmp_ok @m, '>=', 2, 'at least 2 messages'; ok $m[0]->inbound, 'inbound'; -ok ! $m[0]->outbound, 'not inbound'; +ok !$m[0]->outbound, 'not inbound'; like $m[0]->time, qr/^\d{1,2}:\d{2} \w{2}$/, 'time'; is $m[0]->text, 'A', 'text'; ok $m[1]->outbound, 'outbound'; -ok ! $m[1]->inbound, 'not inbound'; +ok !$m[1]->inbound, 'not inbound'; like $m[1]->time, qr/^\d{1,2}:\d{2} \w{2}$/, 'time'; is $m[1]->text, 'A', 'text'; @@ -72,24 +82,20 @@ ok $conv->latest->outbound, 'latest message'; # delete conversation my $id = $conv->id; ok $conv->delete, 'delete'; -isnt +($g->sms)[0]->id, $id, 'deleted'; +isnt + ($g->sms)[0]->id, $id, 'deleted'; # call -ok ! $g->call('invalid #' => 'invalid #'), 'invalid phone numbers'; +ok !$g->call('invalid #' => 'invalid #'), 'invalid phone numbers'; is $@, 'Cannot complete call.', 'error'; ok my $call = $g->call($from_phone => $to_phone), 'call'; ok $call->cancel, 'cancel'; # special feeds -ok +($g->all)[0], 'all feed'; -ok +($g->spam)[0], 'spam feed'; +ok + ($g->all)[0], 'all feed'; +ok + ($g->spam)[0], 'spam feed'; # all other feeds -for my $feed ( qw/ recorded placed received missed starred trash / ) { - ok my $node = ($g->$feed)[0], "$feed feed item"; - is $node->type, $feed, 'type'; +for my $feed (qw/ recorded placed received missed starred trash /) { + ok my $node = ($g->$feed)[0], "$feed feed item"; + is $node->type, $feed, 'type'; } - -#ok $g->logout, 'logout'; - -done_testing;