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

release-1.10release-1.7release-1.8release-1.9
Last change on this file since e4951ea was e4951ea, checked in by Nelson Elhage <nelhage@mit.edu>, 15 years ago
Fix use of weak references.
  • Property mode set to 100644
File size: 8.6 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;
[159aaad]17use HTML::Entities;
18
[7424a5b]19use Scalar::Util qw(weaken);
20
[159aaad]21use BarnOwl;
22use BarnOwl::Message::Twitter;
[7430aa4]23
[159aaad]24sub fail {
25    my $self = shift;
26    my $msg = shift;
27    undef $self->{twitter};
[7430aa4]28    my $nickname = $self->{cfg}->{account_nickname} || "";
29    die("[Twitter $nickname] Error: $msg\n");
[159aaad]30}
31
32sub new {
33    my $class = shift;
34    my $cfg = shift;
35
[385dd69]36    my $val;
37
38    if(!exists $cfg->{default} &&
39       defined($val = delete $cfg->{default_sender})) {
40        $cfg->{default} = $val;
41    }
42
43    if(!exists $cfg->{show_mentions} &&
44       defined($val = delete $cfg->{show_unsubscribed_replies})) {
45        $cfg->{show_mentions} = $val;
46    }
47
[efcd223]48    $cfg = {
49        account_nickname => '',
[385dd69]50        default          => 0,
[efcd223]51        poll_for_tweets  => 1,
52        poll_for_dms     => 1,
[26f9e2e]53        publish_tweets   => 0,
[385dd69]54        show_mentions    => 1,
[efcd223]55        %$cfg
56       };
57
[7430aa4]58    my $self = {
[159aaad]59        'cfg'  => $cfg,
60        'twitter' => undef,
61        'last_id' => undef,
62        'last_direct' => undef,
[36546fa]63        'timer'        => undef,
64        'direct_timer' => undef
[7430aa4]65    };
66
67    bless($self, $class);
[159aaad]68
69    my %twitter_args = @_;
70
[0b13bbc]71    $self->{twitter}  = Net::Twitter::Lite->new(%twitter_args);
[159aaad]72
[82e0f26]73    my $timeline = eval { $self->{twitter}->friends_timeline({count => 1}) };
74    warn "$@" if $@;
[a8a0b0a]75
76    if(!defined($timeline)) {
[7430aa4]77        $self->fail("Invalid credentials");
[159aaad]78    }
79
[a8a0b0a]80    eval {
81        $self->{last_id} = $timeline->[0]{id};
82    };
83    $self->{last_id} = 1 unless defined($self->{last_id});
[159aaad]84
[a8a0b0a]85    eval {
86        $self->{last_direct} = $self->{twitter}->direct_messages()->[0]{id};
87    };
[82e0f26]88    warn "$@" if $@;
[a8a0b0a]89    $self->{last_direct} = 1 unless defined($self->{last_direct});
[159aaad]90
91    eval {
[7430aa4]92        $self->{twitter}->{ua}->timeout(1);
[159aaad]93    };
[82e0f26]94    warn "$@" if $@;
[159aaad]95
[36546fa]96    $self->sleep(0);
97
[7430aa4]98    return $self;
[159aaad]99}
100
[36546fa]101=head2 sleep SECONDS
102
103Stop polling Twitter for SECONDS seconds.
104
105=cut
106
107sub sleep {
108    my $self  = shift;
109    my $delay = shift;
110
[e4951ea]111    my $weak = $self;
112    weaken($weak);
[7424a5b]113
[36546fa]114    if($self->{cfg}->{poll_for_tweets}) {
115        $self->{timer} = BarnOwl::Timer->new({
116            after    => $delay,
117            interval => 60,
[7424a5b]118            cb       => sub { $weak->poll_twitter if $weak }
[36546fa]119           });
120    }
121
122    if($self->{cfg}->{poll_for_dms}) {
123        $self->{direct_timer} = BarnOwl::Timer->new({
124            after    => $delay,
125            interval => 120,
[7424a5b]126            cb       => sub { $weak->poll_direct if $weak }
[36546fa]127           });
128    }
129}
130
[159aaad]131sub twitter_error {
132    my $self = shift;
133
[82e0f26]134    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
135    warn "$@" if $@;
[159aaad]136    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
[36546fa]137        # Twitter's probably just sucking, try again later.
[d69c37c]138        $self->sleep(5*60);
[159aaad]139        return;
140    }
[36546fa]141
[159aaad]142    if(exists($ratelimit->{remaining_hits})
143       && $ratelimit->{remaining_hits} <= 0) {
[d69c37c]144        $self->sleep($ratelimit->{reset_time_in_seconds} - time + 60);
[159aaad]145        die("Twitter: ratelimited until " . $ratelimit->{reset_time} . "\n");
146    } elsif(exists($ratelimit->{error})) {
[36546fa]147        $self->sleep(60*20);
[159aaad]148        die("Twitter: ". $ratelimit->{error} . "\n");
149    }
150}
151
152sub poll_twitter {
153    my $self = shift;
154
155    return unless BarnOwl::getvar('twitter:poll') eq 'on';
156
[36546fa]157    BarnOwl::debug("Polling " . $self->{cfg}->{account_nickname});
158
[82e0f26]159    my $timeline = eval { $self->{twitter}->friends_timeline( { since_id => $self->{last_id} } ) };
160    warn "$@" if $@;
[159aaad]161    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
162        $self->twitter_error();
163        return;
164    };
[5da6ed8]165
[385dd69]166    if ($self->{cfg}->{show_mentions}) {
[82e0f26]167        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
168        warn "$@" if $@;
[5da6ed8]169        unless (defined($mentions) && ref($mentions) eq 'ARRAY') {
170            $self->twitter_error();
171            return;
172        };
173        #combine, sort by id, and uniq
174        push @$timeline, @$mentions;
175        @$timeline = sort { $b->{id} <=> $a->{id} } @$timeline;
176        my $prev = { id => 0 };
177        @$timeline = grep($_->{id} != $prev->{id} && (($prev) = $_), @$timeline);
178    }
179
[159aaad]180    if ( scalar @$timeline ) {
181        for my $tweet ( reverse @$timeline ) {
182            if ( $tweet->{id} <= $self->{last_id} ) {
183                next;
184            }
185            my $msg = BarnOwl::Message->new(
186                type      => 'Twitter',
187                sender    => $tweet->{user}{screen_name},
188                recipient => $self->{cfg}->{user} || $self->{user},
189                direction => 'in',
190                source    => decode_entities($tweet->{source}),
191                location  => decode_entities($tweet->{user}{location}||""),
192                body      => decode_entities($tweet->{text}),
193                status_id => $tweet->{id},
194                service   => $self->{cfg}->{service},
195                account   => $self->{cfg}->{account_nickname},
196               );
197            BarnOwl::queue_message($msg);
198        }
199        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
200    } else {
201        # BarnOwl::message("No new tweets...");
202    }
203}
204
205sub poll_direct {
206    my $self = shift;
207
208    return unless BarnOwl::getvar('twitter:poll') eq 'on';
209
[36546fa]210    BarnOwl::debug("Polling direct for " . $self->{cfg}->{account_nickname});
211
[82e0f26]212    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
213    warn "$@" if $@;
[159aaad]214    unless(defined($direct) && ref($direct) eq 'ARRAY') {
215        $self->twitter_error();
216        return;
217    };
218    if ( scalar @$direct ) {
219        for my $tweet ( reverse @$direct ) {
220            if ( $tweet->{id} <= $self->{last_direct} ) {
221                next;
222            }
223            my $msg = BarnOwl::Message->new(
224                type      => 'Twitter',
225                sender    => $tweet->{sender}{screen_name},
226                recipient => $self->{cfg}->{user} || $self->{user},
227                direction => 'in',
228                location  => decode_entities($tweet->{sender}{location}||""),
229                body      => decode_entities($tweet->{text}),
230                isprivate => 'true',
231                service   => $self->{cfg}->{service},
232                account   => $self->{cfg}->{account_nickname},
233               );
234            BarnOwl::queue_message($msg);
235        }
236        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
237    } else {
238        # BarnOwl::message("No new tweets...");
239    }
240}
241
242sub twitter {
243    my $self = shift;
244
245    my $msg = shift;
246    my $reply_to = shift;
247
248    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
249        $self->twitter_direct($1, $2);
250    } elsif(defined $self->{twitter}) {
[0b13bbc]251        if(defined($reply_to)) {
[159aaad]252            $self->{twitter}->update({
253                status => $msg,
254                in_reply_to_status_id => $reply_to
255               });
256        } else {
257            $self->{twitter}->update($msg);
258        }
259    }
260}
261
262sub twitter_direct {
263    my $self = shift;
264
265    my $who = shift;
266    my $msg = shift;
267    if(defined $self->{twitter}) {
268        $self->{twitter}->new_direct_message({
269            user => $who,
270            text => $msg
271           });
272        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
273            my $tweet = BarnOwl::Message->new(
274                type      => 'Twitter',
275                sender    => $self->{cfg}->{user} || $self->{user},
276                recipient => $who, 
277                direction => 'out',
278                body      => $msg,
279                isprivate => 'true',
280                service   => $self->{cfg}->{service},
281               );
282            BarnOwl::queue_message($tweet);
283        }
284    }
285}
286
287sub twitter_atreply {
288    my $self = shift;
289
290    my $to  = shift;
291    my $id  = shift;
292    my $msg = shift;
293    if(defined($id)) {
294        $self->twitter("@".$to." ".$msg, $id);
295    } else {
296        $self->twitter("@".$to." ".$msg);
297    }
298}
299
[513da71]300sub twitter_follow {
301    my $self = shift;
302
303    my $who = shift;
304
305    my $user = $self->{twitter}->create_friend($who);
306    # returns a string on error
307    if (defined $user && !ref $user) {
308        BarnOwl::message($user);
309    } else {
310        BarnOwl::message("Following " . $who);
311    }
312}
313
314sub twitter_unfollow {
315    my $self = shift;
316
317    my $who = shift;
318
319    my $user = $self->{twitter}->destroy_friend($who);
320    # returns a string on error
321    if (defined $user && !ref $user) {
322        BarnOwl::message($user);
323    } else {
324        BarnOwl::message("No longer following " . $who);
325    }
326}
327
[8462b38]328sub nickname {
329    my $self = shift;
330    return $self->{cfg}->{account_nickname};
331}
332
[159aaad]3331;
Note: See TracBrowser for help on using the repository browser.