* add new hash members to return value of or_search and mentions_ids
[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                     user_id     => $res->{user}{id},
159                 };
160                 if ($res->{retweeted_status}) {
161                     $id->{retweet_of}   = $res->{retweeted_status}{id};
162                     $id->{type}         = 'retweet';
163                 }
164                 else {
165                     $id->{type} = 'search';
166                 }
167                 $ids->{$tweet->{id}} = $id;
168             }
169         }
170     };
171     if ($@) {
172         evalrescue($@);
173     }
174     
175     DEBUG and warn "search result => ", Dumper($ids);
176     return $ids;
177 }
178
179 sub mentions_ids {
180     # return status_ids mentioned to me
181     #   param   => Net::Twitter::Lite object, since_id
182     #   ret     => HashRef of status_id (timeline order is destroyed)
183     #               or undef (none is found)
184     
185     my $bot      = shift @_;
186     my $since_id = shift @_ || 1;
187     
188     my $res;
189     eval {
190         $res = $bot->mentions(
191             {
192                 since_id    => $since_id,
193             }
194         );
195         VERBOSE and warn Dumper($res);
196     };
197     if ($@) {
198         evalrescue($@);
199     }
200     
201     my $ids = {};
202     if ($res && @{$res}) {
203         $ids = {
204             map {
205                 $_->{id} => {
206                     date        => str2time($_->{created_at}),
207                     screen_name => $_->{user}{screen_name},
208                     status_id   => $_->{id},
209                     text        => $_->{text},
210                     type        => 'mention',
211                     user_id     => $_->{user}{id},
212                 }
213             } @{$res}
214         };
215     }
216     
217     DEBUG and warn "mentions result => ", Dumper($ids);
218     return $ids;
219 }
220
221 sub evalrescue {
222     # output error message at eval error
223     
224     use Scalar::Util qw(blessed);
225     
226     if (blessed $@ && $@->isa('Net::Twitter::Lite::Error')) {
227         warn $@->error;
228         if ($@->twitter_error) {
229             my %twitter_error = %{$@->twitter_error};
230             map {
231                 $twitter_error{"$_ => "} = $twitter_error{$_} . "\n";
232                 delete $twitter_error{$_}
233             } keys %twitter_error;
234             warn join("", %twitter_error);
235         }
236     }
237     else {
238         warn $@;
239     }
240 }