source: lib/BarnOwl/Module/Twitter/Handle.pm @ d69c37c

release-1.10release-1.7release-1.8release-1.9
Last change on this file since d69c37c was d69c37c, checked in by Nelson Elhage <nelhage@mit.edu>, 14 years ago
Fix timeouts when we encounter errors.
  • Property mode set to 100644
File size: 8.5 KB
Line 
1use warnings;
2use strict;
3
4=head1 NAME
5
6BarnOwl::Module::Twitter::Handle
7
8=head1 DESCRIPTION
9
10Contains everything needed to send and receive messages from a Twitter-like service.
11
12=cut
13
14package BarnOwl::Module::Twitter::Handle;
15
16use Net::Twitter::Lite;
17use HTML::Entities;
18
19use BarnOwl;
20use BarnOwl::Message::Twitter;
21
22sub fail {
23    my $self = shift;
24    my $msg = shift;
25    undef $self->{twitter};
26    my $nickname = $self->{cfg}->{account_nickname} || "";
27    die("[Twitter $nickname] Error: $msg\n");
28}
29
30sub new {
31    my $class = shift;
32    my $cfg = shift;
33
34    my $val;
35
36    if(!exists $cfg->{default} &&
37       defined($val = delete $cfg->{default_sender})) {
38        $cfg->{default} = $val;
39    }
40
41    if(!exists $cfg->{show_mentions} &&
42       defined($val = delete $cfg->{show_unsubscribed_replies})) {
43        $cfg->{show_mentions} = $val;
44    }
45
46    $cfg = {
47        account_nickname => '',
48        default          => 0,
49        poll_for_tweets  => 1,
50        poll_for_dms     => 1,
51        publish_tweets   => 0,
52        show_mentions    => 1,
53        %$cfg
54       };
55
56    my $self = {
57        'cfg'  => $cfg,
58        'twitter' => undef,
59        'last_id' => undef,
60        'last_direct' => undef,
61        'timer'        => undef,
62        'direct_timer' => undef
63    };
64
65    bless($self, $class);
66
67    my %twitter_args = @_;
68
69    $self->{twitter}  = Net::Twitter::Lite->new(%twitter_args);
70
71    my $timeline = eval { $self->{twitter}->friends_timeline({count => 1}) };
72    warn "$@" if $@;
73
74    if(!defined($timeline)) {
75        $self->fail("Invalid credentials");
76    }
77
78    eval {
79        $self->{last_id} = $timeline->[0]{id};
80    };
81    $self->{last_id} = 1 unless defined($self->{last_id});
82
83    eval {
84        $self->{last_direct} = $self->{twitter}->direct_messages()->[0]{id};
85    };
86    warn "$@" if $@;
87    $self->{last_direct} = 1 unless defined($self->{last_direct});
88
89    eval {
90        $self->{twitter}->{ua}->timeout(1);
91    };
92    warn "$@" if $@;
93
94    $self->sleep(0);
95
96    return $self;
97}
98
99=head2 sleep SECONDS
100
101Stop polling Twitter for SECONDS seconds.
102
103=cut
104
105sub sleep {
106    my $self  = shift;
107    my $delay = shift;
108
109    if($self->{cfg}->{poll_for_tweets}) {
110        $self->{timer} = BarnOwl::Timer->new({
111            after    => $delay,
112            interval => 60,
113            cb       => sub { $self->poll_twitter }
114           });
115    }
116
117    if($self->{cfg}->{poll_for_dms}) {
118        $self->{direct_timer} = BarnOwl::Timer->new({
119            after    => $delay,
120            interval => 120,
121            cb       => sub { $self->poll_direct }
122           });
123    }
124}
125
126sub twitter_error {
127    my $self = shift;
128
129    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
130    warn "$@" if $@;
131    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
132        # Twitter's probably just sucking, try again later.
133        $self->sleep(5*60);
134        return;
135    }
136
137    if(exists($ratelimit->{remaining_hits})
138       && $ratelimit->{remaining_hits} <= 0) {
139        $self->sleep($ratelimit->{reset_time_in_seconds} - time + 60);
140        die("Twitter: ratelimited until " . $ratelimit->{reset_time} . "\n");
141    } elsif(exists($ratelimit->{error})) {
142        $self->sleep(60*20);
143        die("Twitter: ". $ratelimit->{error} . "\n");
144    }
145}
146
147sub poll_twitter {
148    my $self = shift;
149
150    return unless BarnOwl::getvar('twitter:poll') eq 'on';
151
152    BarnOwl::debug("Polling " . $self->{cfg}->{account_nickname});
153
154    my $timeline = eval { $self->{twitter}->friends_timeline( { since_id => $self->{last_id} } ) };
155    warn "$@" if $@;
156    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
157        $self->twitter_error();
158        return;
159    };
160
161    if ($self->{cfg}->{show_mentions}) {
162        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
163        warn "$@" if $@;
164        unless (defined($mentions) && ref($mentions) eq 'ARRAY') {
165            $self->twitter_error();
166            return;
167        };
168        #combine, sort by id, and uniq
169        push @$timeline, @$mentions;
170        @$timeline = sort { $b->{id} <=> $a->{id} } @$timeline;
171        my $prev = { id => 0 };
172        @$timeline = grep($_->{id} != $prev->{id} && (($prev) = $_), @$timeline);
173    }
174
175    if ( scalar @$timeline ) {
176        for my $tweet ( reverse @$timeline ) {
177            if ( $tweet->{id} <= $self->{last_id} ) {
178                next;
179            }
180            my $msg = BarnOwl::Message->new(
181                type      => 'Twitter',
182                sender    => $tweet->{user}{screen_name},
183                recipient => $self->{cfg}->{user} || $self->{user},
184                direction => 'in',
185                source    => decode_entities($tweet->{source}),
186                location  => decode_entities($tweet->{user}{location}||""),
187                body      => decode_entities($tweet->{text}),
188                status_id => $tweet->{id},
189                service   => $self->{cfg}->{service},
190                account   => $self->{cfg}->{account_nickname},
191               );
192            BarnOwl::queue_message($msg);
193        }
194        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
195    } else {
196        # BarnOwl::message("No new tweets...");
197    }
198}
199
200sub poll_direct {
201    my $self = shift;
202
203    return unless BarnOwl::getvar('twitter:poll') eq 'on';
204
205    BarnOwl::debug("Polling direct for " . $self->{cfg}->{account_nickname});
206
207    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
208    warn "$@" if $@;
209    unless(defined($direct) && ref($direct) eq 'ARRAY') {
210        $self->twitter_error();
211        return;
212    };
213    if ( scalar @$direct ) {
214        for my $tweet ( reverse @$direct ) {
215            if ( $tweet->{id} <= $self->{last_direct} ) {
216                next;
217            }
218            my $msg = BarnOwl::Message->new(
219                type      => 'Twitter',
220                sender    => $tweet->{sender}{screen_name},
221                recipient => $self->{cfg}->{user} || $self->{user},
222                direction => 'in',
223                location  => decode_entities($tweet->{sender}{location}||""),
224                body      => decode_entities($tweet->{text}),
225                isprivate => 'true',
226                service   => $self->{cfg}->{service},
227                account   => $self->{cfg}->{account_nickname},
228               );
229            BarnOwl::queue_message($msg);
230        }
231        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
232    } else {
233        # BarnOwl::message("No new tweets...");
234    }
235}
236
237sub twitter {
238    my $self = shift;
239
240    my $msg = shift;
241    my $reply_to = shift;
242
243    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
244        $self->twitter_direct($1, $2);
245    } elsif(defined $self->{twitter}) {
246        if(defined($reply_to)) {
247            $self->{twitter}->update({
248                status => $msg,
249                in_reply_to_status_id => $reply_to
250               });
251        } else {
252            $self->{twitter}->update($msg);
253        }
254    }
255}
256
257sub twitter_direct {
258    my $self = shift;
259
260    my $who = shift;
261    my $msg = shift;
262    if(defined $self->{twitter}) {
263        $self->{twitter}->new_direct_message({
264            user => $who,
265            text => $msg
266           });
267        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
268            my $tweet = BarnOwl::Message->new(
269                type      => 'Twitter',
270                sender    => $self->{cfg}->{user} || $self->{user},
271                recipient => $who, 
272                direction => 'out',
273                body      => $msg,
274                isprivate => 'true',
275                service   => $self->{cfg}->{service},
276               );
277            BarnOwl::queue_message($tweet);
278        }
279    }
280}
281
282sub twitter_atreply {
283    my $self = shift;
284
285    my $to  = shift;
286    my $id  = shift;
287    my $msg = shift;
288    if(defined($id)) {
289        $self->twitter("@".$to." ".$msg, $id);
290    } else {
291        $self->twitter("@".$to." ".$msg);
292    }
293}
294
295sub twitter_follow {
296    my $self = shift;
297
298    my $who = shift;
299
300    my $user = $self->{twitter}->create_friend($who);
301    # returns a string on error
302    if (defined $user && !ref $user) {
303        BarnOwl::message($user);
304    } else {
305        BarnOwl::message("Following " . $who);
306    }
307}
308
309sub twitter_unfollow {
310    my $self = shift;
311
312    my $who = shift;
313
314    my $user = $self->{twitter}->destroy_friend($who);
315    # returns a string on error
316    if (defined $user && !ref $user) {
317        BarnOwl::message($user);
318    } else {
319        BarnOwl::message("No longer following " . $who);
320    }
321}
322
323sub nickname {
324    my $self = shift;
325    return $self->{cfg}->{account_nickname};
326}
327
3281;
Note: See TracBrowser for help on using the repository browser.