source: lib/BarnOwl/Module/Twitter/Handle.pm @ 0948fa0f

release-1.10release-1.7release-1.8release-1.9
Last change on this file since 0948fa0f was 0948fa0f, checked in by Nelson Elhage <nelhage@mit.edu>, 14 years ago
End all warn() and die() messages with a newline. This prevents perl from appending perl script line information, which looks ugly and is useless to anyone who is not the developer.
  • Property mode set to 100644
File size: 9.5 KB
RevLine 
[159aaad]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
[0b13bbc]16use Net::Twitter::Lite;
[9876953]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}
[159aaad]25use HTML::Entities;
26
27use BarnOwl;
28use BarnOwl::Message::Twitter;
[7430aa4]29
[159aaad]30sub fail {
31    my $self = shift;
32    my $msg = shift;
33    undef $self->{twitter};
[7430aa4]34    my $nickname = $self->{cfg}->{account_nickname} || "";
35    die("[Twitter $nickname] Error: $msg\n");
[159aaad]36}
37
38sub new {
39    my $class = shift;
40    my $cfg = shift;
41
[385dd69]42    my $val;
43
44    if(!exists $cfg->{default} &&
45       defined($val = delete $cfg->{default_sender})) {
46        $cfg->{default} = $val;
47    }
48
49    if(!exists $cfg->{show_mentions} &&
50       defined($val = delete $cfg->{show_unsubscribed_replies})) {
51        $cfg->{show_mentions} = $val;
52    }
53
[efcd223]54    $cfg = {
55        account_nickname => '',
[385dd69]56        default          => 0,
[efcd223]57        poll_for_tweets  => 1,
58        poll_for_dms     => 1,
[26f9e2e]59        publish_tweets   => 0,
[385dd69]60        show_mentions    => 1,
[efcd223]61        %$cfg
62       };
63
[7430aa4]64    my $self = {
[159aaad]65        'cfg'  => $cfg,
66        'twitter' => undef,
67        'last_poll' => 0,
68        'last_direct_poll' => 0,
69        'last_id' => undef,
70        'last_direct' => undef,
[7430aa4]71    };
72
73    bless($self, $class);
[159aaad]74
75    my %twitter_args = @_;
76
[0b13bbc]77    $self->{twitter}  = Net::Twitter::Lite->new(%twitter_args);
[159aaad]78
[9876953]79    my $timeline = eval { $self->{twitter}->home_timeline({count => 1}) };
[0948fa0f]80    warn "$@\n" if $@;
[a8a0b0a]81
82    if(!defined($timeline)) {
[7430aa4]83        $self->fail("Invalid credentials");
[159aaad]84    }
85
[a8a0b0a]86    eval {
87        $self->{last_id} = $timeline->[0]{id};
88    };
89    $self->{last_id} = 1 unless defined($self->{last_id});
[159aaad]90
[a8a0b0a]91    eval {
92        $self->{last_direct} = $self->{twitter}->direct_messages()->[0]{id};
93    };
[0948fa0f]94    warn "$@\n" if $@;
[a8a0b0a]95    $self->{last_direct} = 1 unless defined($self->{last_direct});
[159aaad]96
97    eval {
[7430aa4]98        $self->{twitter}->{ua}->timeout(1);
[159aaad]99    };
[0948fa0f]100    warn "$@\n" if $@;
[159aaad]101
[7430aa4]102    return $self;
[159aaad]103}
104
[118d800]105=head2 twitter_command COMMAND ARGS...
106
107Call the specified method on $self->{twitter} with an extended
108timeout. This is intended for interactive commands, with the theory
109that if the user explicitly requested an action, it is slightly more
110acceptable to hang the UI for a second or two than to fail just
111because Twitter is being slightly slow. Polling commands should be
112called normally, with the default (short) timeout, to prevent
113background Twitter suckage from hosing the UI normally.
114
115=cut
116
117sub twitter_command {
118    my $self = shift;
119    my $cmd = shift;
120
121    eval { $self->{twitter}->{ua}->timeout(5); };
122    my $result = eval {
123        $self->{twitter}->$cmd(@_);
124    };
125    my $error = $@;
126    eval { $self->{twitter}->{ua}->timeout(1); };
127    if ($error) {
128        die($error);
129    }
130    return $result;
131}
132
[159aaad]133sub twitter_error {
134    my $self = shift;
135
[82e0f26]136    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
[0948fa0f]137    warn "$@\n" if $@;
[159aaad]138    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
139        # Twitter's just sucking, sleep for 5 minutes
140        $self->{last_direct_poll} = $self->{last_poll} = time + 60*5;
141        # die("Twitter seems to be having problems.\n");
142        return;
143    }
144    if(exists($ratelimit->{remaining_hits})
145       && $ratelimit->{remaining_hits} <= 0) {
146        $self->{last_direct_poll} = $self->{last_poll} = $ratelimit->{reset_time_in_seconds};
147        die("Twitter: ratelimited until " . $ratelimit->{reset_time} . "\n");
148    } elsif(exists($ratelimit->{error})) {
149        die("Twitter: ". $ratelimit->{error} . "\n");
150        $self->{last_direct_poll} = $self->{last_poll} = time + 60*20;
151    }
152}
153
154sub poll_twitter {
155    my $self = shift;
156
157    return unless ( time - $self->{last_poll} ) >= 60;
158    $self->{last_poll} = time;
159    return unless BarnOwl::getvar('twitter:poll') eq 'on';
160
[9876953]161    my $timeline = eval { $self->{twitter}->home_timeline( { since_id => $self->{last_id} } ) };
[0948fa0f]162    warn "$@\n" if $@;
[159aaad]163    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
164        $self->twitter_error();
165        return;
166    };
[5da6ed8]167
[385dd69]168    if ($self->{cfg}->{show_mentions}) {
[82e0f26]169        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
[0948fa0f]170        warn "$@\n" if $@;
[5da6ed8]171        unless (defined($mentions) && ref($mentions) eq 'ARRAY') {
172            $self->twitter_error();
173            return;
174        };
175        #combine, sort by id, and uniq
176        push @$timeline, @$mentions;
177        @$timeline = sort { $b->{id} <=> $a->{id} } @$timeline;
178        my $prev = { id => 0 };
179        @$timeline = grep($_->{id} != $prev->{id} && (($prev) = $_), @$timeline);
180    }
181
[159aaad]182    if ( scalar @$timeline ) {
183        for my $tweet ( reverse @$timeline ) {
184            if ( $tweet->{id} <= $self->{last_id} ) {
185                next;
186            }
187            my $msg = BarnOwl::Message->new(
188                type      => 'Twitter',
189                sender    => $tweet->{user}{screen_name},
190                recipient => $self->{cfg}->{user} || $self->{user},
191                direction => 'in',
192                source    => decode_entities($tweet->{source}),
193                location  => decode_entities($tweet->{user}{location}||""),
194                body      => decode_entities($tweet->{text}),
195                status_id => $tweet->{id},
196                service   => $self->{cfg}->{service},
197                account   => $self->{cfg}->{account_nickname},
198               );
199            BarnOwl::queue_message($msg);
200        }
201        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
202    } else {
203        # BarnOwl::message("No new tweets...");
204    }
205}
206
207sub poll_direct {
208    my $self = shift;
209
210    return unless ( time - $self->{last_direct_poll}) >= 120;
211    $self->{last_direct_poll} = time;
212    return unless BarnOwl::getvar('twitter:poll') eq 'on';
213
[82e0f26]214    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
[0948fa0f]215    warn "$@\n" if $@;
[159aaad]216    unless(defined($direct) && ref($direct) eq 'ARRAY') {
217        $self->twitter_error();
218        return;
219    };
220    if ( scalar @$direct ) {
221        for my $tweet ( reverse @$direct ) {
222            if ( $tweet->{id} <= $self->{last_direct} ) {
223                next;
224            }
225            my $msg = BarnOwl::Message->new(
226                type      => 'Twitter',
227                sender    => $tweet->{sender}{screen_name},
228                recipient => $self->{cfg}->{user} || $self->{user},
229                direction => 'in',
230                location  => decode_entities($tweet->{sender}{location}||""),
231                body      => decode_entities($tweet->{text}),
[c78d06f]232                private => 'true',
[159aaad]233                service   => $self->{cfg}->{service},
234                account   => $self->{cfg}->{account_nickname},
235               );
236            BarnOwl::queue_message($msg);
237        }
238        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
239    } else {
240        # BarnOwl::message("No new tweets...");
241    }
242}
243
244sub twitter {
245    my $self = shift;
246
247    my $msg = shift;
248    my $reply_to = shift;
249
250    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
251        $self->twitter_direct($1, $2);
252    } elsif(defined $self->{twitter}) {
[7ec65f5]253        if(length($msg) > 140) {
254            die("Twitter: Message over 140 characters long.\n");
255        }
[118d800]256        $self->twitter_command('update', {
257            status => $msg,
258            defined($reply_to) ? (in_reply_to_status_id => $reply_to) : ()
259           });
[159aaad]260    }
261}
262
263sub twitter_direct {
264    my $self = shift;
265
266    my $who = shift;
267    my $msg = shift;
268    if(defined $self->{twitter}) {
[118d800]269        $self->twitter_command('new_direct_message', {
[159aaad]270            user => $who,
271            text => $msg
272           });
273        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
274            my $tweet = BarnOwl::Message->new(
275                type      => 'Twitter',
276                sender    => $self->{cfg}->{user} || $self->{user},
277                recipient => $who, 
278                direction => 'out',
279                body      => $msg,
[c78d06f]280                private => 'true',
[159aaad]281                service   => $self->{cfg}->{service},
282               );
283            BarnOwl::queue_message($tweet);
284        }
285    }
286}
287
288sub twitter_atreply {
289    my $self = shift;
290
291    my $to  = shift;
292    my $id  = shift;
293    my $msg = shift;
294    if(defined($id)) {
295        $self->twitter("@".$to." ".$msg, $id);
296    } else {
297        $self->twitter("@".$to." ".$msg);
298    }
299}
300
[5214546]301sub twitter_retweet {
302    my $self = shift;
303    my $msg = shift;
304
305    if($msg->service ne $self->{cfg}->{service}) {
306        die("Cannot retweet a message from a different service.\n");
307    }
308    $self->twitter_command(retweet => $msg->{status_id});
309}
310
[513da71]311sub twitter_follow {
312    my $self = shift;
313
314    my $who = shift;
315
[118d800]316    my $user = $self->twitter_command('create_friend', $who);
[513da71]317    # returns a string on error
318    if (defined $user && !ref $user) {
319        BarnOwl::message($user);
320    } else {
321        BarnOwl::message("Following " . $who);
322    }
323}
324
325sub twitter_unfollow {
326    my $self = shift;
327
328    my $who = shift;
329
[118d800]330    my $user = $self->twitter_command('destroy_friend', $who);
[513da71]331    # returns a string on error
332    if (defined $user && !ref $user) {
333        BarnOwl::message($user);
334    } else {
335        BarnOwl::message("No longer following " . $who);
336    }
337}
338
[8462b38]339sub nickname {
340    my $self = shift;
341    return $self->{cfg}->{account_nickname};
342}
343
[159aaad]3441;
Note: See TracBrowser for help on using the repository browser.