source: lib/BarnOwl/Module/Twitter/Handle.pm @ 118d800

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