source: lib/BarnOwl/Module/Twitter/Handle.pm @ 05cfc78

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