#! /usr/bin/perl -w use strict; use warnings; use utf8; ## IMPORTANT ## # When Net::Twitter::Lite encounters a Twitter API error or a network error, # it throws a Net::Twitter::Lite::Error object. # You can catch and process these exceptions by using eval blocks and testing $@ ## from http://search.cpan.org/perldoc?Net::Twitter::Lite#ERROR_HANDLING use Net::Twitter::Lite::WithAPIv1_1; use FindBin qw($Bin); use YAML::Tiny; use Date::Parse qw(str2time); use Mail::Sendmail; use Encode; my $_execmode = $ARGV[0] || 0; sub VERBOSE () { $_execmode eq 'verbose' }; sub DEBUG () { VERBOSE or $_execmode eq 'debug' }; use Data::Dumper; DEBUG and warn "$0: debug mode"; my $conf = loadconf("$Bin/config.yml"); if (! defined $conf) { die "$0: cannot parse config file.\n"; } my $stat = loadconf("$Bin/status.yml"); if (! defined $stat) { $stat = {}; } my $bot = login($conf); if (! $bot->authorized) { die "$0: this client is not yet authorized.\n"; } my $tweets = {}; %$tweets = ( %$tweets, %{ or_search($bot, $conf->{hashtag}, $stat->{search}) } ); %$tweets = ( %$tweets, %{ mentions_ids($bot, $stat->{mention}) } ); foreach my $id (sort keys %$tweets) { # $tweets->{$id}{type} eq 'search' => found by search API # eq 'mention' => found by mention API my $skip; if ($tweets->{$id}{type} eq 'retweet') { DEBUG and warn "skipping $id that was already retweeted\n"; $skip = 'retweet'; } if (defined $conf->{allow}) { $skip = 'allow'; foreach my $screen_name ( @{ $conf->{allow}{screen_name} } ) { if ($tweets->{$id}{screen_name} eq $screen_name) { DEBUG and warn "$id was allowed by screen_name\n"; undef $skip; last; } } foreach my $user_id ( @{ $conf->{allow}{user_id} } ) { if ($tweets->{$id}{user_id} eq $user_id) { DEBUG and warn "$id was allowed by user_id\n"; undef $skip; last; } } } if (defined $conf->{mail}) { my $send; foreach my $pickup ( @{ $conf->{mail}{pickup} } ) { if ($tweets->{$id}{type} eq $pickup) { $send = 1; last; } } if ($conf->{mail}{ignore_allowed}) { if (defined $conf->{allow} and ! defined $skip) { # this tweet was allowed to retweet, so that be ignored on mail undef $send; } } if ($send) { if (! exists($conf->{mail}{body}) ) { $conf->{mail}{body} = "[localtime(date)] http://twitter.com//status/ text\n" . "----------------------------------------\n" ; } $conf->{mail}{body} .= "[" . localtime($tweets->{$id}{date}) . "] " . "http://twitter.com/" . $tweets->{$id}{screen_name} . "/status/" . $tweets->{$id}{status_id} . " " . $tweets->{$id}{text} . "\n" ; } } if ($skip) { $stat->{$tweets->{$id}{type}} = $id; next; } DEBUG or sleep($conf->{sleep}); # do retweet found tweets my $res; eval { DEBUG or $res = $bot->retweet($id); DEBUG and warn "retweet($id) => ", Dumper($tweets->{$id}); }; if ($@) { evalrescue($@); warn "status_id => $id\n"; next; } $stat->{$tweets->{$id}{type}} = $id; } if ($conf->{mail}{body}) { my $body = encode("iso-2022-jp", $conf->{mail}{body}); my %mail = ( Smtp => $conf->{mail}{server}, From => $conf->{mail}{from}, To => join(", ", @{ $conf->{mail}{to} }), Subject => $conf->{mail}{subject}, "Content-Type" => $conf->{mail}{contenttype}, Body => $body, ); DEBUG and warn "sending mail => ", Dumper(\%mail); DEBUG or sendmail(%mail) or warn "Error sending mail: $Mail::Sendmail::error\n"; } if (ref $tweets and keys %{$tweets}) { # save last status to yaml file DEBUG or YAML::Tiny::DumpFile("$Bin/status.yml", $stat); DEBUG and warn "status.yml => ", Dumper($stat); } sub loadconf { # load configration data from yaml formatted file # param => scalar string of filename # ret => hash object of yaml data my $file = shift @_; my $yaml = YAML::Tiny->read($file); if ($!) { warn "$0: '$file' $!\n"; } DEBUG and warn "'$file' => ", Dumper($yaml); return $yaml->[0]; } sub login { # make Net::Twitter::Lite::WithAPIv1_1 object and login # param => hash object of configration # ret => Net::Twitter::Lite::WithAPIv1_1 object my $conf = shift @_; my $bot = Net::Twitter::Lite::WithAPIv1_1->new( consumer_key => $conf->{consumer_key}, consumer_secret => $conf->{consumer_secret}, legacy_lists_api => 0, ); $bot->access_token($conf->{access_token}); $bot->access_token_secret($conf->{access_token_secret}); return $bot; } sub or_search { # search tweets containing keywords # param => Net::Twitter::Lite::WithAPIv1_1 object, ArrayRef of keywords, since_id # ret => HashRef of status_id (timeline order is destroyed) # or undef (none is found) my $bot = shift @_; my $keywords = shift @_; my $since_id = shift @_ || 1; my $key = ""; foreach my $word (@$keywords) { if ($key) { $key .= " OR $word"; } else { $key = $word; } } DEBUG and warn "searching '$key'"; my $res; my $ids = {}; eval { if ($key) { $res = $bot->search( { q => $key, since_id => $since_id, } ); } VERBOSE and warn Dumper($res); if ($res->{statuses}) { foreach my $tweet (@{$res->{statuses}}) { my $res = $bot->show_status($tweet->{id}); VERBOSE and warn Dumper($res); my $id = { date => str2time($res->{created_at}), screen_name => $res->{user}{screen_name}, status_id => $res->{id}, text => $res->{text}, user_id => $res->{user}{id}, }; if ($res->{retweeted_status}) { $id->{retweet_of} = $res->{retweeted_status}{id}; $id->{type} = 'retweet'; } else { $id->{type} = 'search'; } $ids->{$tweet->{id}} = $id; } } }; if ($@) { evalrescue($@); } DEBUG and warn "search result => ", Dumper($ids); return $ids; } sub mentions_ids { # return status_ids mentioned to me # param => Net::Twitter::Lite::WithAPIv1_1 object, since_id # ret => HashRef of status_id (timeline order is destroyed) # or undef (none is found) my $bot = shift @_; my $since_id = shift @_ || 1; my $res; eval { $res = $bot->mentions( { since_id => $since_id, } ); VERBOSE and warn Dumper($res); }; if ($@) { evalrescue($@); } my $ids = {}; if ($res && @{$res}) { $ids = { map { $_->{id} => { date => str2time($_->{created_at}), screen_name => $_->{user}{screen_name}, status_id => $_->{id}, text => $_->{text}, type => 'mention', user_id => $_->{user}{id}, } } @{$res} }; } DEBUG and warn "mentions result => ", Dumper($ids); return $ids; } sub evalrescue { # output error message at eval error use Scalar::Util qw(blessed); if (blessed $@ && $@->isa('Net::Twitter::Lite::Error')) { warn $@->error; if ($@->twitter_error) { my $twitter_error = $@->twitter_error; if (defined $twitter_error->{errors} && ref($twitter_error->{errors})) { foreach my $error (@{$twitter_error->{errors}}) { warn "code => " , $error->{code}, "\n"; warn "message => ", $error->{message}, "\n"; } } else { # unknown HASH structure use Data::Dumper; warn Dumper $twitter_error; } } } else { warn $@; } }