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

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