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