+#! /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;
+use FindBin qw($Bin);
+use YAML::Tiny;
+use Date::Parse qw(str2time);
+
+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 = {};
+my $tweet;
+
+$tweet = or_search($bot, $conf->{hashtag}, $stat->{search});
+if ($tweet) {
+ %$tweets = (%$tweets, %$tweet);
+}
+
+$tweet = mentions_ids($bot, $stat->{mention});
+if ($tweet) {
+ %$tweets = (%$tweets, %$tweet);
+}
+
+foreach my $id (sort keys %$tweets) {
+ # $tweets->{$id}{type} eq 'search' => found by search API
+ # eq 'mention' => found by mention API
+ if ($tweets->{$id}{type} eq 'retweet') {
+ 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 ($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";
+ }
+
+ return $yaml->[0];
+}
+
+sub login {
+ # make Net::Twitter::Lite object and login
+ # param => hash object of configration
+ # ret => Net::Twitter::Lite object
+
+ my $conf = shift @_;
+
+ my $bot = Net::Twitter::Lite->new(
+ consumer_key => $conf->{consumer_key},
+ consumer_secret => $conf->{consumer_secret},
+ );
+
+ $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 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->{results}) {
+ foreach my $tweet (@{$res->{results}}) {
+ 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 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};
+ map {
+ $twitter_error{"$_ => "} = $twitter_error{$_} . "\n";
+ delete $twitter_error{$_}
+ } keys %twitter_error;
+ warn join("", %twitter_error);
+ }
+ }
+ else {
+ warn $@;
+ }
+}