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

release-1.10release-1.7release-1.8release-1.9
Last change on this file since 385cce2 was c8d9f84, checked in by David Benjamin <davidben@mit.edu>, 14 years ago
More aggressively stop Timers created in perl In case something leaks with references or whatever, at least we don't leak timers.
  • Property mode set to 100644
File size: 12.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
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    if($self->{cfg}->{poll_for_tweets}) {
163        $self->{timer} = BarnOwl::Timer->new({
164            after    => $delay,
165            interval => 90,
166            cb       => sub { $weak->poll_twitter if $weak }
167           });
168    }
169
170    if($self->{cfg}->{poll_for_dms}) {
171        $self->{direct_timer} = BarnOwl::Timer->new({
172            after    => $delay,
173            interval => 180,
174            cb       => sub { $weak->poll_direct if $weak }
175           });
176    }
177}
178
179=head2 twitter_command COMMAND ARGS...
180
181Call the specified method on $self->{twitter} with an extended
182timeout. This is intended for interactive commands, with the theory
183that if the user explicitly requested an action, it is slightly more
184acceptable to hang the UI for a second or two than to fail just
185because Twitter is being slightly slow. Polling commands should be
186called normally, with the default (short) timeout, to prevent
187background Twitter suckage from hosing the UI normally.
188
189=cut
190
191sub twitter_command {
192    my $self = shift;
193    my $cmd = shift;
194
195    eval { $self->{twitter}->{ua}->timeout(5); };
196    my $result = eval {
197        $self->{twitter}->$cmd(@_);
198    };
199    my $error = $@;
200    eval { $self->{twitter}->{ua}->timeout(1); };
201    if ($error) {
202        die($error);
203    }
204    return $result;
205}
206
207sub twitter_error {
208    my $self = shift;
209
210    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
211    BarnOwl::debug($@) if $@;
212    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
213        # Twitter's probably just sucking, try again later.
214        $self->sleep(5*60);
215        return;
216    }
217
218    if(exists($ratelimit->{remaining_hits})
219       && $ratelimit->{remaining_hits} <= 0) {
220        my $timeout = $ratelimit->{reset_time_in_seconds};
221        $self->sleep($timeout - time + 60);
222        BarnOwl::error("Twitter" .
223                       ($self->{cfg}->{account_nickname} ?
224                        "[$self->{cfg}->{account_nickname}]" : "") .
225                        ": ratelimited until " . asctime(localtime($timeout)));
226    } elsif(exists($ratelimit->{error})) {
227        $self->sleep(60*20);
228        die("Twitter: ". $ratelimit->{error} . "\n");
229    }
230}
231
232sub poll_twitter {
233    my $self = shift;
234
235    return unless BarnOwl::getvar('twitter:poll') eq 'on';
236
237    BarnOwl::debug("Polling " . $self->{cfg}->{account_nickname});
238
239    my $timeline = eval { $self->{twitter}->home_timeline( { since_id => $self->{last_id} } ) };
240    BarnOwl::debug($@) if $@;
241    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
242        $self->twitter_error();
243        return;
244    };
245
246    if ($self->{cfg}->{show_mentions}) {
247        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
248        BarnOwl::debug($@) if $@;
249        unless (defined($mentions) && ref($mentions) eq 'ARRAY') {
250            $self->twitter_error();
251            return;
252        };
253        #combine, sort by id, and uniq
254        push @$timeline, @$mentions;
255        @$timeline = sort { $b->{id} <=> $a->{id} } @$timeline;
256        my $prev = { id => 0 };
257        @$timeline = grep($_->{id} != $prev->{id} && (($prev) = $_), @$timeline);
258    }
259
260    if ( scalar @$timeline ) {
261        for my $tweet ( reverse @$timeline ) {
262            if ( $tweet->{id} <= $self->{last_id} ) {
263                next;
264            }
265            my $orig = $tweet->{retweeted_status};
266            $orig = $tweet unless defined($orig);
267
268            my $msg = BarnOwl::Message->new(
269                type      => 'Twitter',
270                sender    => $orig->{user}{screen_name},
271                recipient => $self->{cfg}->{user} || $self->{user},
272                direction => 'in',
273                source    => decode_entities($orig->{source}),
274                location  => decode_entities($orig->{user}{location}||""),
275                body      => decode_entities($orig->{text}),
276                status_id => $tweet->{id},
277                service   => $self->{cfg}->{service},
278                account   => $self->{cfg}->{account_nickname},
279                $tweet->{retweeted_status} ? (retweeted_by => $tweet->{user}{screen_name}) : ()
280               );
281            BarnOwl::queue_message($msg);
282        }
283        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
284    } else {
285        # BarnOwl::message("No new tweets...");
286    }
287}
288
289sub poll_direct {
290    my $self = shift;
291
292    return unless BarnOwl::getvar('twitter:poll') eq 'on';
293
294    BarnOwl::debug("Polling direct for " . $self->{cfg}->{account_nickname});
295
296    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
297    BarnOwl::debug($@) if $@;
298    unless(defined($direct) && ref($direct) eq 'ARRAY') {
299        $self->twitter_error();
300        return;
301    };
302    if ( scalar @$direct ) {
303        for my $tweet ( reverse @$direct ) {
304            if ( $tweet->{id} <= $self->{last_direct} ) {
305                next;
306            }
307            my $msg = BarnOwl::Message->new(
308                type      => 'Twitter',
309                sender    => $tweet->{sender}{screen_name},
310                recipient => $self->{cfg}->{user} || $self->{user},
311                direction => 'in',
312                location  => decode_entities($tweet->{sender}{location}||""),
313                body      => decode_entities($tweet->{text}),
314                private => 'true',
315                service   => $self->{cfg}->{service},
316                account   => $self->{cfg}->{account_nickname},
317               );
318            BarnOwl::queue_message($msg);
319        }
320        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
321    } else {
322        # BarnOwl::message("No new tweets...");
323    }
324}
325
326sub _stripnl {
327    my $msg = shift;
328
329    # strip non-newline whitespace around newlines
330    $msg =~ s/[^\n\S]*(\n+)[^\n\S]*/$1/sg;
331    # change single newlines to a single space; leave multiple newlines
332    $msg =~ s/([^\n])\n([^\n])/$1 $2/sg;
333    # strip leading and trailing whitespace
334    $msg =~ s/\s+$//s;
335    $msg =~ s/^\s+//s;
336
337    return $msg;
338}
339
340sub twitter {
341    my $self = shift;
342
343    my $msg = _stripnl(shift);
344    my $reply_to = shift;
345
346    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
347        $self->twitter_direct($1, $2);
348    } elsif(defined $self->{twitter}) {
349        if(length($msg) > 140) {
350            die("Twitter: Message over 140 characters long.\n");
351        }
352        $self->twitter_command('update', {
353            status => $msg,
354            defined($reply_to) ? (in_reply_to_status_id => $reply_to) : ()
355           });
356    }
357    $self->poll_twitter if $self->{cfg}->{poll_for_tweets};
358}
359
360sub twitter_direct {
361    my $self = shift;
362
363    my $who = shift;
364    my $msg = _stripnl(shift);
365
366    if(defined $self->{twitter}) {
367        $self->twitter_command('new_direct_message', {
368            user => $who,
369            text => $msg
370           });
371        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
372            my $tweet = BarnOwl::Message->new(
373                type      => 'Twitter',
374                sender    => $self->{cfg}->{user} || $self->{user},
375                recipient => $who, 
376                direction => 'out',
377                body      => $msg,
378                private => 'true',
379                service   => $self->{cfg}->{service},
380               );
381            BarnOwl::queue_message($tweet);
382        }
383    }
384}
385
386sub twitter_atreply {
387    my $self = shift;
388
389    my $to  = shift;
390    my $id  = shift;
391    my $msg = shift;
392    if(defined($id)) {
393        $self->twitter($msg, $id);
394    } else {
395        $self->twitter($msg);
396    }
397}
398
399sub twitter_retweet {
400    my $self = shift;
401    my $msg = shift;
402
403    if($msg->service ne $self->{cfg}->{service}) {
404        die("Cannot retweet a message from a different service.\n");
405    }
406    $self->twitter_command(retweet => $msg->{status_id});
407    $self->poll_twitter if $self->{cfg}->{poll_for_tweets};
408}
409
410sub twitter_follow {
411    my $self = shift;
412
413    my $who = shift;
414
415    my $user = $self->twitter_command('create_friend', $who);
416    # returns a string on error
417    if (defined $user && !ref $user) {
418        BarnOwl::message($user);
419    } else {
420        BarnOwl::message("Following " . $who);
421    }
422}
423
424sub twitter_unfollow {
425    my $self = shift;
426
427    my $who = shift;
428
429    my $user = $self->twitter_command('destroy_friend', $who);
430    # returns a string on error
431    if (defined $user && !ref $user) {
432        BarnOwl::message($user);
433    } else {
434        BarnOwl::message("No longer following " . $who);
435    }
436}
437
438sub nickname {
439    my $self = shift;
440    return $self->{cfg}->{account_nickname};
441}
442
4431;
Note: See TracBrowser for help on using the repository browser.