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