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