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

release-1.7release-1.8release-1.9
Last change on this file since c9bdb46 was c9bdb46, checked in by Nelson Elhage <nelhage@mit.edu>, 12 years ago
Be less noisy about warnings. The user almost never cares about these, since they are usually just transient errors from Twitter.
  • Property mode set to 100644
File size: 10.2 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;
17BEGIN {
18    # Backwards compatibility with version of Net::Twitter::Lite that
19    # lack home_timeline.
20    if(!defined(*Net::Twitter::Lite::home_timeline{CODE})) {
21        *Net::Twitter::Lite::home_timeline =
22          \&Net::Twitter::Lite::friends_timeline;
23    }
24}
25use HTML::Entities;
26
27use Scalar::Util qw(weaken);
28
29use BarnOwl;
30use BarnOwl::Message::Twitter;
31use POSIX qw(asctime);
32
33sub fail {
34    my $self = shift;
35    my $msg = shift;
36    undef $self->{twitter};
37    my $nickname = $self->{cfg}->{account_nickname} || "";
38    die("[Twitter $nickname] Error: $msg\n");
39}
40
41sub new {
42    my $class = shift;
43    my $cfg = shift;
44
45    my $val;
46
47    if(!exists $cfg->{default} &&
48       defined($val = delete $cfg->{default_sender})) {
49        $cfg->{default} = $val;
50    }
51
52    if(!exists $cfg->{show_mentions} &&
53       defined($val = delete $cfg->{show_unsubscribed_replies})) {
54        $cfg->{show_mentions} = $val;
55    }
56
57    $cfg = {
58        account_nickname => '',
59        default          => 0,
60        poll_for_tweets  => 1,
61        poll_for_dms     => 1,
62        publish_tweets   => 0,
63        show_mentions    => 1,
64        %$cfg
65       };
66
67    my $self = {
68        'cfg'  => $cfg,
69        'twitter' => undef,
70        'last_id' => undef,
71        'last_direct' => undef,
72        'timer'        => undef,
73        'direct_timer' => undef
74    };
75
76    bless($self, $class);
77
78    my %twitter_args = @_;
79
80    $self->{twitter}  = Net::Twitter::Lite->new(%twitter_args);
81
82    my $timeline = eval { $self->{twitter}->home_timeline({count => 1}) };
83    warn "$@\n" if $@;
84
85    if(!defined($timeline)) {
86        $self->fail("Invalid credentials");
87    }
88
89    eval {
90        $self->{last_id} = $timeline->[0]{id};
91    };
92    $self->{last_id} = 1 unless defined($self->{last_id});
93
94    eval {
95        $self->{last_direct} = $self->{twitter}->direct_messages()->[0]{id};
96    };
97    warn "$@\n" if $@;
98    $self->{last_direct} = 1 unless defined($self->{last_direct});
99
100    eval {
101        $self->{twitter}->{ua}->timeout(1);
102    };
103    warn "$@\n" if $@;
104
105    $self->sleep(0);
106
107    return $self;
108}
109
110=head2 sleep SECONDS
111
112Stop polling Twitter for SECONDS seconds.
113
114=cut
115
116sub sleep {
117    my $self  = shift;
118    my $delay = shift;
119
120    my $weak = $self;
121    weaken($weak);
122
123    if($self->{cfg}->{poll_for_tweets}) {
124        $self->{timer} = BarnOwl::Timer->new({
125            after    => $delay,
126            interval => 90,
127            cb       => sub { $weak->poll_twitter if $weak }
128           });
129    }
130
131    if($self->{cfg}->{poll_for_dms}) {
132        $self->{direct_timer} = BarnOwl::Timer->new({
133            after    => $delay,
134            interval => 180,
135            cb       => sub { $weak->poll_direct if $weak }
136           });
137    }
138}
139
140=head2 twitter_command COMMAND ARGS...
141
142Call the specified method on $self->{twitter} with an extended
143timeout. This is intended for interactive commands, with the theory
144that if the user explicitly requested an action, it is slightly more
145acceptable to hang the UI for a second or two than to fail just
146because Twitter is being slightly slow. Polling commands should be
147called normally, with the default (short) timeout, to prevent
148background Twitter suckage from hosing the UI normally.
149
150=cut
151
152sub twitter_command {
153    my $self = shift;
154    my $cmd = shift;
155
156    eval { $self->{twitter}->{ua}->timeout(5); };
157    my $result = eval {
158        $self->{twitter}->$cmd(@_);
159    };
160    my $error = $@;
161    eval { $self->{twitter}->{ua}->timeout(1); };
162    if ($error) {
163        die($error);
164    }
165    return $result;
166}
167
168sub twitter_error {
169    my $self = shift;
170
171    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
172    BarnOwl::debug($@) if $@;
173    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
174        # Twitter's probably just sucking, try again later.
175        $self->sleep(5*60);
176        return;
177    }
178
179    if(exists($ratelimit->{remaining_hits})
180       && $ratelimit->{remaining_hits} <= 0) {
181        my $timeout = $ratelimit->{reset_time_in_seconds};
182        $self->sleep($timeout - time + 60);
183        BarnOwl::error("Twitter" .
184                       ($self->{cfg}->{account_nickname} ?
185                        "[$self->{cfg}->{account_nickname}]" : "") .
186                        ": ratelimited until " . asctime(localtime($timeout)));
187    } elsif(exists($ratelimit->{error})) {
188        $self->sleep(60*20);
189        die("Twitter: ". $ratelimit->{error} . "\n");
190    }
191}
192
193sub poll_twitter {
194    my $self = shift;
195
196    return unless BarnOwl::getvar('twitter:poll') eq 'on';
197
198    BarnOwl::debug("Polling " . $self->{cfg}->{account_nickname});
199
200    my $timeline = eval { $self->{twitter}->home_timeline( { since_id => $self->{last_id} } ) };
201    BarnOwl::debug($@) if $@;
202    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
203        $self->twitter_error();
204        return;
205    };
206
207    if ($self->{cfg}->{show_mentions}) {
208        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
209        BarnOwl::debug($@) if $@;
210        unless (defined($mentions) && ref($mentions) eq 'ARRAY') {
211            $self->twitter_error();
212            return;
213        };
214        #combine, sort by id, and uniq
215        push @$timeline, @$mentions;
216        @$timeline = sort { $b->{id} <=> $a->{id} } @$timeline;
217        my $prev = { id => 0 };
218        @$timeline = grep($_->{id} != $prev->{id} && (($prev) = $_), @$timeline);
219    }
220
221    if ( scalar @$timeline ) {
222        for my $tweet ( reverse @$timeline ) {
223            if ( $tweet->{id} <= $self->{last_id} ) {
224                next;
225            }
226            my $msg = BarnOwl::Message->new(
227                type      => 'Twitter',
228                sender    => $tweet->{user}{screen_name},
229                recipient => $self->{cfg}->{user} || $self->{user},
230                direction => 'in',
231                source    => decode_entities($tweet->{source}),
232                location  => decode_entities($tweet->{user}{location}||""),
233                body      => decode_entities($tweet->{text}),
234                status_id => $tweet->{id},
235                service   => $self->{cfg}->{service},
236                account   => $self->{cfg}->{account_nickname},
237               );
238            BarnOwl::queue_message($msg);
239        }
240        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
241    } else {
242        # BarnOwl::message("No new tweets...");
243    }
244}
245
246sub poll_direct {
247    my $self = shift;
248
249    return unless BarnOwl::getvar('twitter:poll') eq 'on';
250
251    BarnOwl::debug("Polling direct for " . $self->{cfg}->{account_nickname});
252
253    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
254    BarnOwl::debug($@) if $@;
255    unless(defined($direct) && ref($direct) eq 'ARRAY') {
256        $self->twitter_error();
257        return;
258    };
259    if ( scalar @$direct ) {
260        for my $tweet ( reverse @$direct ) {
261            if ( $tweet->{id} <= $self->{last_direct} ) {
262                next;
263            }
264            my $msg = BarnOwl::Message->new(
265                type      => 'Twitter',
266                sender    => $tweet->{sender}{screen_name},
267                recipient => $self->{cfg}->{user} || $self->{user},
268                direction => 'in',
269                location  => decode_entities($tweet->{sender}{location}||""),
270                body      => decode_entities($tweet->{text}),
271                private => 'true',
272                service   => $self->{cfg}->{service},
273                account   => $self->{cfg}->{account_nickname},
274               );
275            BarnOwl::queue_message($msg);
276        }
277        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
278    } else {
279        # BarnOwl::message("No new tweets...");
280    }
281}
282
283sub twitter {
284    my $self = shift;
285
286    my $msg = shift;
287    my $reply_to = shift;
288
289    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
290        $self->twitter_direct($1, $2);
291    } elsif(defined $self->{twitter}) {
292        if(length($msg) > 140) {
293            die("Twitter: Message over 140 characters long.\n");
294        }
295        $self->twitter_command('update', {
296            status => $msg,
297            defined($reply_to) ? (in_reply_to_status_id => $reply_to) : ()
298           });
299    }
300}
301
302sub twitter_direct {
303    my $self = shift;
304
305    my $who = shift;
306    my $msg = shift;
307    if(defined $self->{twitter}) {
308        $self->twitter_command('new_direct_message', {
309            user => $who,
310            text => $msg
311           });
312        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
313            my $tweet = BarnOwl::Message->new(
314                type      => 'Twitter',
315                sender    => $self->{cfg}->{user} || $self->{user},
316                recipient => $who, 
317                direction => 'out',
318                body      => $msg,
319                private => 'true',
320                service   => $self->{cfg}->{service},
321               );
322            BarnOwl::queue_message($tweet);
323        }
324    }
325}
326
327sub twitter_atreply {
328    my $self = shift;
329
330    my $to  = shift;
331    my $id  = shift;
332    my $msg = shift;
333    if(defined($id)) {
334        $self->twitter("@".$to." ".$msg, $id);
335    } else {
336        $self->twitter("@".$to." ".$msg);
337    }
338}
339
340sub twitter_retweet {
341    my $self = shift;
342    my $msg = shift;
343
344    if($msg->service ne $self->{cfg}->{service}) {
345        die("Cannot retweet a message from a different service.\n");
346    }
347    $self->twitter_command(retweet => $msg->{status_id});
348}
349
350sub twitter_follow {
351    my $self = shift;
352
353    my $who = shift;
354
355    my $user = $self->twitter_command('create_friend', $who);
356    # returns a string on error
357    if (defined $user && !ref $user) {
358        BarnOwl::message($user);
359    } else {
360        BarnOwl::message("Following " . $who);
361    }
362}
363
364sub twitter_unfollow {
365    my $self = shift;
366
367    my $who = shift;
368
369    my $user = $self->twitter_command('destroy_friend', $who);
370    # returns a string on error
371    if (defined $user && !ref $user) {
372        BarnOwl::message($user);
373    } else {
374        BarnOwl::message("No longer following " . $who);
375    }
376}
377
378sub nickname {
379    my $self = shift;
380    return $self->{cfg}->{account_nickname};
381}
382
3831;
Note: See TracBrowser for help on using the repository browser.