Skip to content

Instantly share code, notes, and snippets.

@mtsukamoto
Created May 11, 2011 09:13
Show Gist options
  • Save mtsukamoto/966157 to your computer and use it in GitHub Desktop.
Save mtsukamoto/966157 to your computer and use it in GitHub Desktop.
Showbelx Update to Local Growl for Windows
use utf8;
use strict;
use warnings;
use Encode;
use Growl::GNTP;
use HTTP::Request::Common;
use URI;
use WWW::Mechanize;
use YAML;
use Web::Scraper;
our $VERSION = '0.0.2';
my %args = @ARGV;
my $yaml = exists($args{'-c'}) ? $args{'-c'} : './shovelx2growl.yml';
my $config = load_config($yaml);
my $ua = WWW::Mechanize->new;
$ua->env_proxy;
my $res = &login($config, $ua);
die unless ($res);
my @entries = &fetch($config, $ua);
&notify(@entries);
YAML::DumpFile($yaml, $config);
exit();
sub load_config {
my $yaml = shift;
die sprintf("Config file is not specified.") unless (length($yaml));
unless (-f $yaml) {
my $default = {
'pages' => {
'1' => {'enabled' => '1', 'url' => '/direct_messages', 'title' => 'しゃべる - 受信箱'},
'2' => {'enabled' => '0', 'url' => '/direct_messages/sent', 'title' => 'しゃべる - 送信箱'},
'3' => {'enabled' => '0', 'url' => '/replies', 'title' => 'しゃべる - あなた宛のつぶやき'},
'4' => {'enabled' => '1', 'url' => '/home', 'title' => 'しゃべる - 最近のつぶやき'},
'5' => {'enabled' => '0', 'url' => '/account/archive', 'title' => 'しゃべる - あなたのつぶやき'},
'6' => {'enabled' => '1', 'url' => '/public_timeline', 'title' => 'しゃべる - 公開つぶやき'}
},
'site' => {'account' => 'alibaba', 'password' => 'jUgeMJugEm', 'url' => 'http://shovelx.example.com:80/'}
};
$_->{'title'} = encode_utf8(decode_utf8($_->{'title'})) for (values(%{$default->{'pages'}}));
print "'$yaml' is not exists, creating.\n";
YAML::DumpFile($yaml, $default);
die "Edit '$yaml', then run shovelx2growl again.";
}
$yaml = decode_utf8($yaml);
my $config = YAML::LoadFile($yaml);
die sprintf("File '%s' is not valid.") unless ($config);
return $config;
}
sub login {
my ($config, $ua) = @_;
my $base = URI->new($config->{'site'}->{'url'});
my $account = $config->{'site'}->{'account'};
my $password = $config->{'site'}->{'password'};
# login
my $uri = URI->new_abs('/login', $base);
my $req = HTTP::Request::Common::POST(
$uri,
['account' => $config->{'site'}->{'account'}, 'password' => $config->{'site'}->{'password'}]
);
my $res = $ua->request($req);
die(decode_utf8($res->status_line). "\n") unless ($res->is_success);
&detect_error($ua);
return 1;
}
sub fetch {
my ($config, $ua) = @_;
my @keys = UNIVERSAL::isa($config->{'pages'}, 'HASH') ? keys(%{$config->{'pages'}}) : ();
my @pages = map { $config->{'pages'}->{$_} } sort { $b <=> $a } @keys;
my $statuses = {};
my $messages = {};
foreach my $page (@pages) {
next unless ($page->{'url'} && $page->{'enabled'});
my $title = decode_utf8($page->{'title'});
my $res = &scrape($config, $ua, $page->{'url'});
my ($type) = grep { exists($res->{$_}) } qw(status message);
my @entries = (UNIVERSAL::isa($res->{$type}, 'ARRAY')) ? @{$res->{$type}} : ();
next unless (@entries);
# additional field
foreach my $entry (@entries) {
$entry->{'id'} = ($entry->{$type} =~ /(\d+)$/) ? $1 : 0;
$entry->{'title'} = $title;
$entry->{'page'} = $page->{'url'};
}
# unread filtering
my $min = $page->{'recent'} || 0;
@entries = sort { $a->{'id'} <=> $b->{'id'} } grep { $_->{'id'} > $min } @entries;
$page->{'recent'} = $entries[-1]->{'id'} if (@entries);
# map
if ($type eq 'status') { $statuses->{$_->{'id'}} = $_ for (@entries); }
elsif ($type eq 'message') { $messages->{$_->{'id'}} = $_ for (@entries); }
}
my @entries = (
(map { $messages->{$_ } } sort { $a <=> $b } (keys(%$messages))),
(map { $statuses->{$_ } } sort { $a <=> $b } (keys(%$statuses))),
);
return @entries;
}
sub scrape {
my ($config, $ua, $url) = @_;
# NOTE: url shoud be /home, /replies, /account/archive, /public_timeline, /direct_messages, /direct_messages/sent
my $base = URI->new($config->{'site'}->{'url'});
my $uri = URI->new_abs($url, $base);
my $scraper = scraper {
process '#directMessages_timeline table.timeline tbody tr', 'message[]' => scraper {
process 'img.profile-image', 'image' => '@src';
process 'span.enter span', 'name' => '@title';
process 'span.enter span', 'user' => 'TEXT';
process 'span.entry-body', 'body' => 'TEXT';
process 'span.entry-body', 'message' => '@id';
process 'span.dateTime', 'time' => 'TEXT';
};
process '#home_timeline tr.status', 'status[]' => scraper {
process 'img.profile-image', 'image' => '@src';
process 'strong.enter a', 'home' => '@href';
process 'strong.enter span', 'name' => '@title';
process 'strong.enter span', 'user' => 'TEXT';
process 'span.entry-body', 'body' => 'TEXT';
process 'span.entry-body', 'status' => '@id';
process 'span.time', 'time' => 'TEXT';
};
};
$scraper->user_agent($ua);
my $res = $scraper->scrape($uri);
&detect_error($ua);
return $res;
}
sub notify {
my @entries = @_;
return unless (@entries);
my $app = 'ShovelX2Growl';
my $growl = Growl::GNTP->new(AppName => $app);
my %pages = map { $_->{'page'} => 1 } (@entries);
my @events = map { { Name => $_ } } keys(%pages);
$growl->register(\@events);
foreach my $entry (@entries) {
my $notify = {
Event => $entry->{'page'} || '',
Title => sprintf('%s / %s', $entry->{'name'}, $entry->{'user'}),
Message => sprintf("%s\n(%s via %s)", $entry->{'body'}, $entry->{'time'}, $entry->{'title'}),
Icon => $entry->{'image'}->as_string
};
$notify->{$_} = encode_utf8($notify->{$_}) for (keys(%$notify));
$growl->notify(%$notify);
}
}
sub detect_error {
my ($ua) = @_;
if ($ua->content =~ /(<input [^<>]*name="error"[^<>]*>)/) {
my $input = $1;
my $error = ($input =~ /id="(.*?)"/) ? $1 : 'unknown_error';
die(decode_utf8("[ERROR] $error". "\n"));
}
}
1;
__END__
=head1 history
=over
=item ver.0.0.1
Initial release. Status update notification available.
=item ver.0.0.2
Direct message notification available.
=back
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment