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

release-1.10release-1.7release-1.8release-1.9
Last change on this file since d27ecf3 was a2640485, checked in by Nelson Elhage <nelhage@mit.edu>, 15 years ago
Twitter: Insert the "@user" into the editwin on replies. This wasn't possible (or at least wasn't easy) until recently, when BarnOwl::start_edit_win actually changed the context, instead of having to wait for the mainloop to pick it up.
  • Property mode set to 100644
File size: 11.5 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 $msg = BarnOwl::Message->new(
251                type      => 'Twitter',
252                sender    => $tweet->{user}{screen_name},
253                recipient => $self->{cfg}->{user} || $self->{user},
254                direction => 'in',
255                source    => decode_entities($tweet->{source}),
256                location  => decode_entities($tweet->{user}{location}||""),
257                body      => decode_entities($tweet->{text}),
258                status_id => $tweet->{id},
259                service   => $self->{cfg}->{service},
260                account   => $self->{cfg}->{account_nickname},
261               );
262            BarnOwl::queue_message($msg);
263        }
264        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
265    } else {
266        # BarnOwl::message("No new tweets...");
267    }
268}
269
270sub poll_direct {
271    my $self = shift;
272
273    return unless BarnOwl::getvar('twitter:poll') eq 'on';
274
275    BarnOwl::debug("Polling direct for " . $self->{cfg}->{account_nickname});
276
277    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
278    BarnOwl::debug($@) if $@;
279    unless(defined($direct) && ref($direct) eq 'ARRAY') {
280        $self->twitter_error();
281        return;
282    };
283    if ( scalar @$direct ) {
284        for my $tweet ( reverse @$direct ) {
285            if ( $tweet->{id} <= $self->{last_direct} ) {
286                next;
287            }
288            my $msg = BarnOwl::Message->new(
289                type      => 'Twitter',
290                sender    => $tweet->{sender}{screen_name},
291                recipient => $self->{cfg}->{user} || $self->{user},
292                direction => 'in',
293                location  => decode_entities($tweet->{sender}{location}||""),
294                body      => decode_entities($tweet->{text}),
295                private => 'true',
296                service   => $self->{cfg}->{service},
297                account   => $self->{cfg}->{account_nickname},
298               );
299            BarnOwl::queue_message($msg);
300        }
301        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
302    } else {
303        # BarnOwl::message("No new tweets...");
304    }
305}
306
307sub _stripnl {
308    my $msg = shift;
309
310    # strip non-newline whitespace around newlines
311    $msg =~ s/[^\n\S]*(\n+)[^\n\S]*/$1/sg;
312    # change single newlines to a single space; leave multiple newlines
313    $msg =~ s/([^\n])\n([^\n])/$1 $2/sg;
314    # strip leading and trailing whitespace
315    $msg =~ s/\s+$//s;
316    $msg =~ s/^\s+//s;
317
318    return $msg;
319}
320
321sub twitter {
322    my $self = shift;
323
324    my $msg = _stripnl(shift);
325    my $reply_to = shift;
326
327    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
328        $self->twitter_direct($1, $2);
329    } elsif(defined $self->{twitter}) {
330        if(length($msg) > 140) {
331            die("Twitter: Message over 140 characters long.\n");
332        }
333        $self->twitter_command('update', {
334            status => $msg,
335            defined($reply_to) ? (in_reply_to_status_id => $reply_to) : ()
336           });
337    }
338    $self->poll_twitter if $self->{cfg}->{poll_for_tweets};
339}
340
341sub twitter_direct {
342    my $self = shift;
343
344    my $who = shift;
345    my $msg = _stripnl(shift);
346
347    if(defined $self->{twitter}) {
348        $self->twitter_command('new_direct_message', {
349            user => $who,
350            text => $msg
351           });
352        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
353            my $tweet = BarnOwl::Message->new(
354                type      => 'Twitter',
355                sender    => $self->{cfg}->{user} || $self->{user},
356                recipient => $who, 
357                direction => 'out',
358                body      => $msg,
359                private => 'true',
360                service   => $self->{cfg}->{service},
361               );
362            BarnOwl::queue_message($tweet);
363        }
364    }
365}
366
367sub twitter_atreply {
368    my $self = shift;
369
370    my $to  = shift;
371    my $id  = shift;
372    my $msg = shift;
373    if(defined($id)) {
374        $self->twitter($msg, $id);
375    } else {
376        $self->twitter($msg);
377    }
378}
379
380sub twitter_retweet {
381    my $self = shift;
382    my $msg = shift;
383
384    if($msg->service ne $self->{cfg}->{service}) {
385        die("Cannot retweet a message from a different service.\n");
386    }
387    $self->twitter_command(retweet => $msg->{status_id});
388    $self->poll_twitter if $self->{cfg}->{poll_for_tweets};
389}
390
391sub twitter_follow {
392    my $self = shift;
393
394    my $who = shift;
395
396    my $user = $self->twitter_command('create_friend', $who);
397    # returns a string on error
398    if (defined $user && !ref $user) {
399        BarnOwl::message($user);
400    } else {
401        BarnOwl::message("Following " . $who);
402    }
403}
404
405sub twitter_unfollow {
406    my $self = shift;
407
408    my $who = shift;
409
410    my $user = $self->twitter_command('destroy_friend', $who);
411    # returns a string on error
412    if (defined $user && !ref $user) {
413        BarnOwl::message($user);
414    } else {
415        BarnOwl::message("No longer following " . $who);
416    }
417}
418
419sub nickname {
420    my $self = shift;
421    return $self->{cfg}->{account_nickname};
422}
423
4241;
Note: See TracBrowser for help on using the repository browser.