* Dev for wiki:Dev
[lab.git] / Dev / 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 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 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         VERBOSE and warn Dumper($res);
145         if ($res->{results}) {
146             foreach my $tweet (@{$res->{results}}) {
147                 my $res = $bot->show_status($tweet->{id});
148                 VERBOSE and warn Dumper($res);
149                 
150                 my $id = {
151                     date        => str2time($res->{created_at}),
152                     screen_name => $res->{user}{screen_name},
153                     status_id   => $res->{id},
154                     text        => $res->{text},
155                     user_id     => $res->{user}{id},
156                 };
157                 if ($res->{retweeted_status}) {
158                     $id->{retweet_of}   = $res->{retweeted_status}{id};
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                     user_id     => $_->{user}{id},
209                 }
210             } @{$res}
211         };
212     }
213     
214     DEBUG and warn "mentions result => ", Dumper($ids);
215     return $ids;
216 }
217
218 sub evalrescue {
219     # output error message at eval error
220     
221     use Scalar::Util qw(blessed);
222     
223     if (blessed $@ && $@->isa('Net::Twitter::Lite::Error')) {
224         warn $@->error;
225         if ($@->twitter_error) {
226             my %twitter_error = %{$@->twitter_error};
227             map {
228                 $twitter_error{"$_ => "} = $twitter_error{$_} . "\n";
229                 delete $twitter_error{$_}
230             } keys %twitter_error;
231             warn join("", %twitter_error);
232         }
233     }
234     else {
235         warn $@;
236     }
237 }