* remove needless hash check
[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 %$tweets = (
40     %$tweets,
41     %{ or_search($bot, $conf->{hashtag}, $stat->{search}) }
42 );
43 %$tweets = (
44     %$tweets,
45     %{ mentions_ids($bot, $stat->{mention}) }
46 );
47
48 foreach my $id (sort keys %$tweets) {
49     # $tweets->{$id}{type} eq 'search'  => found by search API
50     #                      eq 'mention' => found by mention API
51     if ($tweets->{$id}{type} eq 'retweet') {
52         next;
53     }
54     DEBUG or sleep($conf->{sleep});
55     
56     # do retweet found tweets
57     my $res;
58     eval {
59         DEBUG  or $res = $bot->retweet($id);
60         DEBUG and warn "retweet($id) => ", Dumper($tweets->{$id});
61     };
62     if ($@) {
63         evalrescue($@);
64         warn "status_id => $id\n";
65         next;
66     }
67     
68     $stat->{$tweets->{$id}{type}} = $id;
69 }
70
71 foreach my $word (keys %{$conf->{response}}) {
72     # reply to follower's tweets containing keywords
73     $tweets = follower_search($bot, $word);
74     foreach my $id (sort keys %$tweets) {
75         if ($tweets->{$id}{type} eq 'retweet') {
76             next;
77         }
78         DEBUG or sleep($conf->{sleep});
79         
80         my $text = '@' . $tweets->{$id}{screen_name}
81             . " " . $conf->{response}{$word};
82         my $res;
83         eval {
84             DEBUG  or $res = $bot->update(
85                 {
86                     in_reply_to_status_id   => $id,
87                     status                  => $text,
88                 }
89             );
90             DEBUG and warn "update(", $text, ") => ",
91                 Dumper($tweets->{$id});
92         };
93         if ($@) {
94             evalrescue($@);
95         }
96     }
97 }
98
99 if ($tweets) {
100     # save last status to yaml file
101     DEBUG  or YAML::Tiny::DumpFile("$Bin/status.yml", $stat);
102     DEBUG and warn "status.yml => ", Dumper($stat);
103 }
104
105
106 sub loadconf {
107     # load configration data from yaml formatted file
108     #   param   => scalar string of filename
109     #   ret     => hash object of yaml data
110     
111     my $file = shift @_;
112     
113     my $yaml = YAML::Tiny->read($file);
114     
115     if ($!) {
116         warn "$0: '$file' $!\n";
117     }
118     
119     return $yaml->[0];
120 }
121
122 sub login {
123     # make Net::Twitter::Lite object and login
124     #   param   => hash object of configration
125     #   ret     => Net::Twitter::Lite object
126     
127     my $conf = shift @_;
128     
129     my $bot = Net::Twitter::Lite->new(
130         consumer_key    => $conf->{consumer_key},
131         consumer_secret => $conf->{consumer_secret},
132     );
133     
134     $bot->access_token($conf->{access_token});
135     $bot->access_token_secret($conf->{access_token_secret});
136     
137     return $bot;
138 }
139
140 sub or_search {
141     # search tweets containing keywords
142     #   param   => Net::Twitter::Lite object, ArrayRef of keywords, since_id
143     #   ret     => HashRef of status_id (timeline order is destroyed)
144     #               or undef (none is found)
145     
146     my $bot      = shift @_;
147     my $keywords = shift @_;
148     my $since_id = shift @_ || 1;
149     
150     my $key = "";
151     foreach my $word (@$keywords) {
152         if ($key) {
153             $key .= " OR $word";
154         }
155         else {
156             $key = $word;
157         }
158     }
159     DEBUG and warn "searching '$key'";
160     
161     my $res;
162     my $ids = {};
163     eval {
164         if ($key) {
165             $res = $bot->search(
166                 {
167                     q           => $key,
168                     since_id    => $since_id,
169                 }
170             );
171         }
172         VERBOSE and warn Dumper($res);
173         if ($res->{results}) {
174             foreach my $tweet (@{$res->{results}}) {
175                 my $res = $bot->show_status($tweet->{id});
176                 VERBOSE and warn Dumper($res);
177                 
178                 my $id = {
179                     date        => str2time($res->{created_at}),
180                     screen_name => $res->{user}{screen_name},
181                     status_id   => $res->{id},
182                     text        => $res->{text},
183                     user_id     => $res->{user}{id},
184                 };
185                 if ($res->{retweeted_status}) {
186                     $id->{retweet_of}   = $res->{retweeted_status}{id};
187                     $id->{type}         = 'retweet';
188                 }
189                 else {
190                     $id->{type} = 'search';
191                 }
192                 $ids->{$tweet->{id}} = $id;
193             }
194         }
195     };
196     if ($@) {
197         evalrescue($@);
198     }
199     
200     DEBUG and warn "search result => ", Dumper($ids);
201     return $ids;
202 }
203
204 sub mentions_ids {
205     # return status_ids mentioned to me
206     #   param   => Net::Twitter::Lite object, since_id
207     #   ret     => HashRef of status_id (timeline order is destroyed)
208     #               or undef (none is found)
209     
210     my $bot      = shift @_;
211     my $since_id = shift @_ || 1;
212     
213     my $res;
214     eval {
215         $res = $bot->mentions(
216             {
217                 since_id    => $since_id,
218             }
219         );
220         VERBOSE and warn Dumper($res);
221     };
222     if ($@) {
223         evalrescue($@);
224     }
225     
226     my $ids = {};
227     if ($res && @{$res}) {
228         $ids = {
229             map {
230                 $_->{id} => {
231                     date        => str2time($_->{created_at}),
232                     screen_name => $_->{user}{screen_name},
233                     status_id   => $_->{id},
234                     text        => $_->{text},
235                     type        => 'mention',
236                     user_id     => $_->{user}{id},
237                 }
238             } @{$res}
239         };
240     }
241     
242     DEBUG and warn "mentions result => ", Dumper($ids);
243     return $ids;
244 }
245
246 sub follower_search {
247     # search follower's tweets containing keywords
248     #   param   => Net::Twitter::Lite object, keyword string, since_id
249     #   ret     => HashRef of status_id (timeline order is destroyed)
250     #               or undef (none is found)
251     
252     my $bot      = shift @_;
253     my $keyword  = shift @_;
254     my $since_id = shift @_ || 1;
255     
256     my $tweets = or_search($bot, [ $keyword ], $since_id);
257     if (! $tweets) {
258         return {};
259     }
260     
261     my $followers;
262     eval {
263         $followers = $bot->followers_ids();
264         VERBOSE and warn Dumper($followers);
265     };
266     if ($@) {
267         evalrescue($@);
268     }
269     
270     my $ids = {};
271     foreach my $status_id (keys %$tweets) {
272         foreach my $user_id (@$followers) {
273             if ($tweets->{$status_id}{user_id} == $user_id) {
274                 $ids->{$status_id} = $tweets->{$status_id};
275                 last;
276             }
277         }
278     }
279     
280     DEBUG and warn "search result in followers => ", Dumper($ids);
281     return $ids;
282 }
283
284 sub evalrescue {
285     # output error message at eval error
286     
287     use Scalar::Util qw(blessed);
288     
289     if (blessed $@ && $@->isa('Net::Twitter::Lite::Error')) {
290         warn $@->error;
291         if ($@->twitter_error) {
292             my %twitter_error = %{$@->twitter_error};
293             map {
294                 $twitter_error{"$_ => "} = $twitter_error{$_} . "\n";
295                 delete $twitter_error{$_}
296             } keys %twitter_error;
297             warn join("", %twitter_error);
298         }
299     }
300     else {
301         warn $@;
302     }
303 }