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

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