source: lib/BarnOwl/Module/Twitter/Handle.pm @ e4e1dcb

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