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