* FIX: warning 'Use of uninitialized value $ARGV[0]' on non debug mode
[lab.git] / twitterbot.pl
1 #! /usr/bin/perl -w
2
3 use strict;
4 use warnings;
5 use utf8;
6
7 ## IMPORTANT ##
8 # When Net::Twitter::Lite encounters a Twitter API error or a network error, 
9 # it throws a Net::Twitter::Lite::Error object. 
10 # You can catch and process these exceptions by using eval blocks and testing $@
11 ## from http://search.cpan.org/perldoc?Net::Twitter::Lite#ERROR_HANDLING
12 use Net::Twitter::Lite;
13 use FindBin qw($Bin);
14 use YAML::Tiny;
15 use Date::Parse qw(str2time);
16
17 my $_execmode = $ARGV[0] || 0;
18 sub VERBOSE () { $_execmode eq 'verbose' };
19 sub DEBUG   () { VERBOSE or $_execmode eq 'debug' };
20 use Data::Dumper;
21
22 DEBUG and warn "$0: debug mode";
23
24 my $conf = loadconf("$Bin/config.yml");
25 if (! defined $conf) {
26     die "$0: cannot parse config file.\n";
27 }
28 my $stat = loadconf("$Bin/status.yml");
29 if (! defined $stat) {
30     $stat = {};
31 }
32
33 my $bot = login($conf);
34 if (! $bot->authorized) {
35     die "$0: this client is not yet authorized.\n";
36 }
37
38 my $tweets = {};
39 my $tweet;
40
41 $tweet = or_search($bot, $conf->{hashtag}, $stat->{search});
42 if ($tweet) {
43     %$tweets = (%$tweets, %$tweet);
44 }
45
46 $tweet = mentions_ids($bot, $stat->{mention});
47 if ($tweet) {
48     %$tweets = (%$tweets, %$tweet);
49 }
50
51 foreach my $id (sort keys %$tweets) {
52     # $tweets->{$id}{type} eq 'search'  => found by search API
53     #                      eq 'mention' => found by mention API
54     if ($tweets->{$id}{type} eq 'retweet') {
55         next;
56     }
57     DEBUG or sleep($conf->{sleep});
58     
59     # do retweet found tweets
60     my $res;
61     eval {
62         DEBUG  or $res = $bot->retweet($id);
63         DEBUG and warn "retweet($id) => ", Dumper($tweets->{$id});
64     };
65     if ($@) {
66         evalrescue($@);
67         warn "status_id => $id\n";
68         next;
69     }
70     
71     $stat->{$tweets->{$id}{type}} = $id;
72 }
73
74 if ($tweets) {
75     # save last status to yaml file
76     DEBUG  or YAML::Tiny::DumpFile("$Bin/status.yml", $stat);
77     DEBUG and warn "status.yml => ", Dumper($stat);
78 }
79
80
81 sub loadconf {
82     # load configration data from yaml formatted file
83     #   param   => scalar string of filename
84     #   ret     => hash object of yaml data
85     
86     my $file = shift @_;
87     
88     my $yaml = YAML::Tiny->read($file);
89     
90     if ($!) {
91         warn "$0: '$file' $!\n";
92     }
93     
94     return $yaml->[0];
95 }
96
97 sub login {
98     # make Net::Twitter::Lite object and login
99     #   param   => hash object of configration
100     #   ret     => Net::Twitter::Lite object
101     
102     my $conf = shift @_;
103     
104     my $bot = Net::Twitter::Lite->new(
105         consumer_key    => $conf->{consumer_key},
106         consumer_secret => $conf->{consumer_secret},
107     );
108     
109     $bot->access_token($conf->{access_token});
110     $bot->access_token_secret($conf->{access_token_secret});
111     
112     return $bot;
113 }
114
115 sub or_search {
116     # search tweets containing keywords
117     #   param   => Net::Twitter::Lite object, ArrayRef of keywords, since_id
118     #   ret     => HashRef of status_id (timeline order is destroyed)
119     #               or undef (none is found)
120     
121     my $bot      = shift @_;
122     my $keywords = shift @_;
123     my $since_id = shift @_ || 1;
124     
125     my $key = "";
126     foreach my $word (@$keywords) {
127         if ($key) {
128             $key .= " OR $word";
129         }
130         else {
131             $key = $word;
132         }
133     }
134     DEBUG and warn "searching '$key'";
135     
136     my $res;
137     my $ids = {};
138     eval {
139         if ($key) {
140             $res = $bot->search(
141                 {
142                     q           => $key,
143                     since_id    => $since_id,
144                 }
145             );
146         }
147         VERBOSE and warn Dumper($res);
148         if ($res->{results}) {
149             foreach my $tweet (@{$res->{results}}) {
150                 my $res = $bot->show_status($tweet->{id});
151                 VERBOSE and warn Dumper($res);
152                 
153                 my $id = {
154                     date        => str2time($res->{created_at}),
155                     screen_name => $res->{user}{screen_name},
156                     status_id   => $res->{id},
157                     text        => $res->{text},
158                 };
159                 if ($res->{retweeted_status}) {
160                     $id->{type} = 'retweet';
161                 }
162                 else {
163                     $id->{type} = 'search';
164                 }
165                 $ids->{$tweet->{id}} = $id;
166             }
167         }
168     };
169     if ($@) {
170         evalrescue($@);
171     }
172     
173     DEBUG and warn "search result => ", Dumper($ids);
174     return $ids;
175 }
176
177 sub mentions_ids {
178     # return status_ids mentioned to me
179     #   param   => Net::Twitter::Lite object, since_id
180     #   ret     => HashRef of status_id (timeline order is destroyed)
181     #               or undef (none is found)
182     
183     my $bot      = shift @_;
184     my $since_id = shift @_ || 1;
185     
186     my $res;
187     eval {
188         $res = $bot->mentions(
189             {
190                 since_id    => $since_id,
191             }
192         );
193         VERBOSE and warn Dumper($res);
194     };
195     if ($@) {
196         evalrescue($@);
197     }
198     
199     my $ids = {};
200     if ($res && @{$res}) {
201         $ids = {
202             map {
203                 $_->{id} => {
204                     date        => str2time($_->{created_at}),
205                     screen_name => $_->{user}{screen_name},
206                     status_id   => $_->{id},
207                     text        => $_->{text},
208                     type        => 'mention',
209                 }
210             } @{$res}
211         };
212     }
213     
214     DEBUG and warn "mentions result => ", Dumper($ids);
215     return $ids;
216 }
217
218 sub evalrescue {
219     # output error message at eval error
220     
221     use Scalar::Util qw(blessed);
222     
223     if (blessed $@ && $@->isa('Net::Twitter::Lite::Error')) {
224         warn $@->error;
225         if ($@->twitter_error) {
226             my %twitter_error = %{$@->twitter_error};
227             map {
228                 $twitter_error{"$_ => "} = $twitter_error{$_} . "\n";
229                 delete $twitter_error{$_}
230             } keys %twitter_error;
231             warn join("", %twitter_error);
232         }
233     }
234     else {
235         warn $@;
236     }
237 }