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

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