source: perl/modules/Twitter/lib/BarnOwl/Module/Twitter/Handle.pm @ c6adf17

release-1.7release-1.8release-1.9
Last change on this file since c6adf17 was c6adf17, checked in by David Benjamin <davidben@mit.edu>, 11 years ago
Track names along with timers, add :show timers This will help people with BarnOwls eating CPU to diagnose timer leaks.
  • Property mode set to 100644
File size: 12.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
33use constant BARNOWL_CONSUMER_KEY    => "9Py27vCQl6uB5V7ijmp31A";
34use constant BARNOWL_CONSUMER_SECRET => "GLhheSim8P5cVuk9FTM99KTEgWLW0LGl7gf54QWfg";
35
36sub fail {
37    my $self = shift;
38    my $msg = shift;
39    undef $self->{twitter};
40    my $nickname = $self->{cfg}->{account_nickname} || "";
41    die("[Twitter $nickname] Error: $msg\n");
42}
43
44sub new {
45    my $class = shift;
46    my $cfg = shift;
47
48    my $val;
49
50    if(!exists $cfg->{default} &&
51       defined($val = delete $cfg->{default_sender})) {
52        $cfg->{default} = $val;
53    }
54
55    if(!exists $cfg->{show_mentions} &&
56       defined($val = delete $cfg->{show_unsubscribed_replies})) {
57        $cfg->{show_mentions} = $val;
58    }
59
60    $cfg = {
61        account_nickname => '',
62        default          => 0,
63        poll_for_tweets  => 1,
64        poll_for_dms     => 1,
65        publish_tweets   => 0,
66        show_mentions    => 1,
67        oauth_key        => BARNOWL_CONSUMER_KEY,
68        oauth_secret     => BARNOWL_CONSUMER_SECRET,
69        %$cfg,
70       };
71
72    my $self = {
73        'cfg'  => $cfg,
74        'twitter' => undef,
75        'last_id' => undef,
76        'last_direct' => undef,
77        'timer'        => undef,
78        'direct_timer' => undef
79    };
80
81    bless($self, $class);
82
83    my %twitter_args = @_;
84
85    my ($username, $password, $xauth);
86
87    if ($cfg->{service} eq 'http://twitter.com') {
88        BarnOwl::debug('Checking for xAuth support in Net::Twitter');
89        if (*Net::Twitter::Lite::xauth{CODE}) {
90            $xauth = 1;
91            $username = delete $twitter_args{username};
92            $password = delete $twitter_args{password};
93            $twitter_args{consumer_key}    = $cfg->{oauth_key};
94            $twitter_args{consumer_secret} = $cfg->{oauth_secret};
95        } else {
96            BarnOwl::error("Please upgrade your version of Net::Twitter::Lite to support xAuth.");
97        }
98    }
99
100    $self->{twitter}  = Net::Twitter::Lite->new(%twitter_args,);
101
102    if ($xauth){
103        eval {
104            $self->{twitter}->xauth($username, $password);
105        };
106        if($@) {
107            $self->fail("Invalid credentials: $@");
108        }
109    }
110
111    my $timeline = eval { $self->{twitter}->home_timeline({count => 1}) };
112    warn "$@\n" if $@;
113
114    if(!defined($timeline) && !$xauth) {
115        $self->fail("Invalid credentials");
116    }
117
118    eval {
119        $self->{last_id} = $timeline->[0]{id};
120    };
121    $self->{last_id} = 1 unless defined($self->{last_id});
122
123    eval {
124        $self->{last_direct} = $self->{twitter}->direct_messages()->[0]{id};
125    };
126    warn "$@\n" if $@;
127    $self->{last_direct} = 1 unless defined($self->{last_direct});
128
129    eval {
130        $self->{twitter}->{ua}->timeout(1);
131    };
132    warn "$@\n" if $@;
133
134    $self->sleep(0);
135
136    return $self;
137}
138
139=head2 sleep SECONDS
140
141Stop polling Twitter for SECONDS seconds.
142
143=cut
144
145sub sleep {
146    my $self  = shift;
147    my $delay = shift;
148
149    my $weak = $self;
150    weaken($weak);
151
152    # Stop any existing timers.
153    if (defined $self->{timer}) {
154        $self->{timer}->stop;
155        $self->{timer} = undef;
156    }
157    if (defined $self->{direct_timer}) {
158        $self->{direct_timer}->stop;
159        $self->{direct_timer} = undef;
160    }
161
162    my $nickname = $self->{cfg}->{account_nickname};
163    if($self->{cfg}->{poll_for_tweets}) {
164        $self->{timer} = BarnOwl::Timer->new({
165            name     => "Twitter ($nickname) poll_for_tweets",
166            after    => $delay,
167            interval => 90,
168            cb       => sub { $weak->poll_twitter if $weak }
169           });
170    }
171
172    if($self->{cfg}->{poll_for_dms}) {
173        $self->{direct_timer} = BarnOwl::Timer->new({
174            name     => "Twitter ($nickname) poll_for_dms",
175            after    => $delay,
176            interval => 180,
177            cb       => sub { $weak->poll_direct if $weak }
178           });
179    }
180}
181
182=head2 twitter_command COMMAND ARGS...
183
184Call the specified method on $self->{twitter} with an extended
185timeout. This is intended for interactive commands, with the theory
186that if the user explicitly requested an action, it is slightly more
187acceptable to hang the UI for a second or two than to fail just
188because Twitter is being slightly slow. Polling commands should be
189called normally, with the default (short) timeout, to prevent
190background Twitter suckage from hosing the UI normally.
191
192=cut
193
194sub twitter_command {
195    my $self = shift;
196    my $cmd = shift;
197
198    eval { $self->{twitter}->{ua}->timeout(5); };
199    my $result = eval {
200        $self->{twitter}->$cmd(@_);
201    };
202    my $error = $@;
203    eval { $self->{twitter}->{ua}->timeout(1); };
204    if ($error) {
205        die($error);
206    }
207    return $result;
208}
209
210sub twitter_error {
211    my $self = shift;
212
213    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
214    BarnOwl::debug($@) if $@;
215    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
216        # Twitter's probably just sucking, try again later.
217        $self->sleep(5*60);
218        return;
219    }
220
221    if(exists($ratelimit->{remaining_hits})
222       && $ratelimit->{remaining_hits} <= 0) {
223        my $timeout = $ratelimit->{reset_time_in_seconds};
224        $self->sleep($timeout - time + 60);
225        BarnOwl::error("Twitter" .
226                       ($self->{cfg}->{account_nickname} ?
227                        "[$self->{cfg}->{account_nickname}]" : "") .
228                        ": ratelimited until " . asctime(localtime($timeout)));
229    } elsif(exists($ratelimit->{error})) {
230        $self->sleep(60*20);
231        die("Twitter: ". $ratelimit->{error} . "\n");
232    }
233}
234
235sub poll_twitter {
236    my $self = shift;
237
238    return unless BarnOwl::getvar('twitter:poll') eq 'on';
239
240    BarnOwl::debug("Polling " . $self->{cfg}->{account_nickname});
241
242    my $timeline = eval { $self->{twitter}->home_timeline( { since_id => $self->{last_id} } ) };
243    BarnOwl::debug($@) if $@;
244    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
245        $self->twitter_error();
246        return;
247    };
248
249    if ($self->{cfg}->{show_mentions}) {
250        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
251        BarnOwl::debug($@) if $@;
252        unless (defined($mentions) && ref($mentions) eq 'ARRAY') {
253            $self->twitter_error();
254            return;
255        };
256        #combine, sort by id, and uniq
257        push @$timeline, @$mentions;
258        @$timeline = sort { $b->{id} <=> $a->{id} } @$timeline;
259        my $prev = { id => 0 };
260        @$timeline = grep($_->{id} != $prev->{id} && (($prev) = $_), @$timeline);
261    }
262
263    if ( scalar @$timeline ) {
264        for my $tweet ( reverse @$timeline ) {
265            if ( $tweet->{id} <= $self->{last_id} ) {
266                next;
267            }
268            my $orig = $tweet->{retweeted_status};
269            $orig = $tweet unless defined($orig);
270
271            my $msg = BarnOwl::Message->new(
272                type      => 'Twitter',
273                sender    => $orig->{user}{screen_name},
274                recipient => $self->{cfg}->{user} || $self->{user},
275                direction => 'in',
276                source    => decode_entities($orig->{source}),
277                location  => decode_entities($orig->{user}{location}||""),
278                body      => decode_entities($orig->{text}),
279                status_id => $tweet->{id},
280                service   => $self->{cfg}->{service},
281                account   => $self->{cfg}->{account_nickname},
282                $tweet->{retweeted_status} ? (retweeted_by => $tweet->{user}{screen_name}) : ()
283               );
284            BarnOwl::queue_message($msg);
285        }
286        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
287    } else {
288        # BarnOwl::message("No new tweets...");
289    }
290}
291
292sub poll_direct {
293    my $self = shift;
294
295    return unless BarnOwl::getvar('twitter:poll') eq 'on';
296
297    BarnOwl::debug("Polling direct for " . $self->{cfg}->{account_nickname});
298
299    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
300    BarnOwl::debug($@) if $@;
301    unless(defined($direct) && ref($direct) eq 'ARRAY') {
302        $self->twitter_error();
303        return;
304    };
305    if ( scalar @$direct ) {
306        for my $tweet ( reverse @$direct ) {
307            if ( $tweet->{id} <= $self->{last_direct} ) {
308                next;
309            }
310            my $msg = BarnOwl::Message->new(
311                type      => 'Twitter',
312                sender    => $tweet->{sender}{screen_name},
313                recipient => $self->{cfg}->{user} || $self->{user},
314                direction => 'in',
315                location  => decode_entities($tweet->{sender}{location}||""),
316                body      => decode_entities($tweet->{text}),
317                private => 'true',
318                service   => $self->{cfg}->{service},
319                account   => $self->{cfg}->{account_nickname},
320               );
321            BarnOwl::queue_message($msg);
322        }
323        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
324    } else {
325        # BarnOwl::message("No new tweets...");
326    }
327}
328
329sub _stripnl {
330    my $msg = shift;
331
332    # strip non-newline whitespace around newlines
333    $msg =~ s/[^\n\S]*(\n+)[^\n\S]*/$1/sg;
334    # change single newlines to a single space; leave multiple newlines
335    $msg =~ s/([^\n])\n([^\n])/$1 $2/sg;
336    # strip leading and trailing whitespace
337    $msg =~ s/\s+$//s;
338    $msg =~ s/^\s+//s;
339
340    return $msg;
341}
342
343sub twitter {
344    my $self = shift;
345
346    my $msg = _stripnl(shift);
347    my $reply_to = shift;
348
349    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
350        $self->twitter_direct($1, $2);
351    } elsif(defined $self->{twitter}) {
352        if(length($msg) > 140) {
353            die("Twitter: Message over 140 characters long.\n");
354        }
355        $self->twitter_command('update', {
356            status => $msg,
357            defined($reply_to) ? (in_reply_to_status_id => $reply_to) : ()
358           });
359    }
360    $self->poll_twitter if $self->{cfg}->{poll_for_tweets};
361}
362
363sub twitter_direct {
364    my $self = shift;
365
366    my $who = shift;
367    my $msg = _stripnl(shift);
368
369    if(defined $self->{twitter}) {
370        $self->twitter_command('new_direct_message', {
371            user => $who,
372            text => $msg
373           });
374        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
375            my $tweet = BarnOwl::Message->new(
376                type      => 'Twitter',
377                sender    => $self->{cfg}->{user} || $self->{user},
378                recipient => $who, 
379                direction => 'out',
380                body      => $msg,
381                private => 'true',
382                service   => $self->{cfg}->{service},
383               );
384            BarnOwl::queue_message($tweet);
385        }
386    }
387}
388
389sub twitter_atreply {
390    my $self = shift;
391
392    my $to  = shift;
393    my $id  = shift;
394    my $msg = shift;
395    if(defined($id)) {
396        $self->twitter($msg, $id);
397    } else {
398        $self->twitter($msg);
399    }
400}
401
402sub twitter_retweet {
403    my $self = shift;
404    my $msg = shift;
405
406    if($msg->service ne $self->{cfg}->{service}) {
407        die("Cannot retweet a message from a different service.\n");
408    }
409    $self->twitter_command(retweet => $msg->{status_id});
410    $self->poll_twitter if $self->{cfg}->{poll_for_tweets};
411}
412
413sub twitter_follow {
414    my $self = shift;
415
416    my $who = shift;
417
418    my $user = $self->twitter_command('create_friend', $who);
419    # returns a string on error
420    if (defined $user && !ref $user) {
421        BarnOwl::message($user);
422    } else {
423        BarnOwl::message("Following " . $who);
424    }
425}
426
427sub twitter_unfollow {
428    my $self = shift;
429
430    my $who = shift;
431
432    my $user = $self->twitter_command('destroy_friend', $who);
433    # returns a string on error
434    if (defined $user && !ref $user) {
435        BarnOwl::message($user);
436    } else {
437        BarnOwl::message("No longer following " . $who);
438    }
439}
440
441sub nickname {
442    my $self = shift;
443    return $self->{cfg}->{account_nickname};
444}
445
4461;
Note: See TracBrowser for help on using the repository browser.