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>, 14 years ago
Fix use of weak references.
  • Property mode set to 100644
File size: 8.6 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;
17use HTML::Entities;
18
19use Scalar::Util qw(weaken);
20
21use BarnOwl;
22use BarnOwl::Message::Twitter;
23
24sub fail {
25    my $self = shift;
26    my $msg = shift;
27    undef $self->{twitter};
28    my $nickname = $self->{cfg}->{account_nickname} || "";
29    die("[Twitter $nickname] Error: $msg\n");
30}
31
32sub new {
33    my $class = shift;
34    my $cfg = shift;
35
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
48    $cfg = {
49        account_nickname => '',
50        default          => 0,
51        poll_for_tweets  => 1,
52        poll_for_dms     => 1,
53        publish_tweets   => 0,
54        show_mentions    => 1,
55        %$cfg
56       };
57
58    my $self = {
59        'cfg'  => $cfg,
60        'twitter' => undef,
61        'last_id' => undef,
62        'last_direct' => undef,
63        'timer'        => undef,
64        'direct_timer' => undef
65    };
66
67    bless($self, $class);
68
69    my %twitter_args = @_;
70
71    $self->{twitter}  = Net::Twitter::Lite->new(%twitter_args);
72
73    my $timeline = eval { $self->{twitter}->friends_timeline({count => 1}) };
74    warn "$@" if $@;
75
76    if(!defined($timeline)) {
77        $self->fail("Invalid credentials");
78    }
79
80    eval {
81        $self->{last_id} = $timeline->[0]{id};
82    };
83    $self->{last_id} = 1 unless defined($self->{last_id});
84
85    eval {
86        $self->{last_direct} = $self->{twitter}->direct_messages()->[0]{id};
87    };
88    warn "$@" if $@;
89    $self->{last_direct} = 1 unless defined($self->{last_direct});
90
91    eval {
92        $self->{twitter}->{ua}->timeout(1);
93    };
94    warn "$@" if $@;
95
96    $self->sleep(0);
97
98    return $self;
99}
100
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
111    my $weak = $self;
112    weaken($weak);
113
114    if($self->{cfg}->{poll_for_tweets}) {
115        $self->{timer} = BarnOwl::Timer->new({
116            after    => $delay,
117            interval => 60,
118            cb       => sub { $weak->poll_twitter if $weak }
119           });
120    }
121
122    if($self->{cfg}->{poll_for_dms}) {
123        $self->{direct_timer} = BarnOwl::Timer->new({
124            after    => $delay,
125            interval => 120,
126            cb       => sub { $weak->poll_direct if $weak }
127           });
128    }
129}
130
131sub twitter_error {
132    my $self = shift;
133
134    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
135    warn "$@" if $@;
136    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
137        # Twitter's probably just sucking, try again later.
138        $self->sleep(5*60);
139        return;
140    }
141
142    if(exists($ratelimit->{remaining_hits})
143       && $ratelimit->{remaining_hits} <= 0) {
144        $self->sleep($ratelimit->{reset_time_in_seconds} - time + 60);
145        die("Twitter: ratelimited until " . $ratelimit->{reset_time} . "\n");
146    } elsif(exists($ratelimit->{error})) {
147        $self->sleep(60*20);
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
157    BarnOwl::debug("Polling " . $self->{cfg}->{account_nickname});
158
159    my $timeline = eval { $self->{twitter}->friends_timeline( { since_id => $self->{last_id} } ) };
160    warn "$@" if $@;
161    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
162        $self->twitter_error();
163        return;
164    };
165
166    if ($self->{cfg}->{show_mentions}) {
167        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
168        warn "$@" if $@;
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
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
210    BarnOwl::debug("Polling direct for " . $self->{cfg}->{account_nickname});
211
212    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
213    warn "$@" if $@;
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}) {
251        if(defined($reply_to)) {
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
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
328sub nickname {
329    my $self = shift;
330    return $self->{cfg}->{account_nickname};
331}
332
3331;
Note: See TracBrowser for help on using the repository browser.