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