source: lib/BarnOwl/Module/Twitter/Handle.pm @ 648e823

release-1.10release-1.7release-1.8release-1.9
Last change on this file since 648e823 was 648e823, checked in by Nelson Elhage <nelhage@mit.edu>, 15 years ago
Trigger a twitter poll on events that generate tweets. This will hopefully lead to a more immediate UI response to local twitter actions.
  • Property mode set to 100644
File size: 10.3 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    $self->poll_twitter;
301}
302
303sub twitter_direct {
304    my $self = shift;
305
306    my $who = shift;
307    my $msg = shift;
308    if(defined $self->{twitter}) {
309        $self->twitter_command('new_direct_message', {
310            user => $who,
311            text => $msg
312           });
313        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
314            my $tweet = BarnOwl::Message->new(
315                type      => 'Twitter',
316                sender    => $self->{cfg}->{user} || $self->{user},
317                recipient => $who, 
318                direction => 'out',
319                body      => $msg,
320                private => 'true',
321                service   => $self->{cfg}->{service},
322               );
323            BarnOwl::queue_message($tweet);
324        }
325    }
326}
327
328sub twitter_atreply {
329    my $self = shift;
330
331    my $to  = shift;
332    my $id  = shift;
333    my $msg = shift;
334    if(defined($id)) {
335        $self->twitter("@".$to." ".$msg, $id);
336    } else {
337        $self->twitter("@".$to." ".$msg);
338    }
339}
340
341sub twitter_retweet {
342    my $self = shift;
343    my $msg = shift;
344
345    if($msg->service ne $self->{cfg}->{service}) {
346        die("Cannot retweet a message from a different service.\n");
347    }
348    $self->twitter_command(retweet => $msg->{status_id});
349    $self->poll_twitter;
350}
351
352sub twitter_follow {
353    my $self = shift;
354
355    my $who = shift;
356
357    my $user = $self->twitter_command('create_friend', $who);
358    # returns a string on error
359    if (defined $user && !ref $user) {
360        BarnOwl::message($user);
361    } else {
362        BarnOwl::message("Following " . $who);
363    }
364}
365
366sub twitter_unfollow {
367    my $self = shift;
368
369    my $who = shift;
370
371    my $user = $self->twitter_command('destroy_friend', $who);
372    # returns a string on error
373    if (defined $user && !ref $user) {
374        BarnOwl::message($user);
375    } else {
376        BarnOwl::message("No longer following " . $who);
377    }
378}
379
380sub nickname {
381    my $self = shift;
382    return $self->{cfg}->{account_nickname};
383}
384
3851;
Note: See TracBrowser for help on using the repository browser.