source: lib/BarnOwl/Module/Twitter/Handle.pm @ 7ec65f5

release-1.10release-1.7release-1.8release-1.9
Last change on this file since 7ec65f5 was 7ec65f5, checked in by Nelson Elhage <nelhage@mit.edu>, 14 years ago
Don't allow updates of > 140 characters. Twitter seems to have recently stopped truncating these messages, and started just dropping them on the floor. This is very confusing, and so check for them on our end.
  • Property mode set to 100644
File size: 8.2 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
97sub twitter_error {
98    my $self = shift;
99
100    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
101    warn "$@" if $@;
102    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
103        # Twitter's just sucking, sleep for 5 minutes
104        $self->{last_direct_poll} = $self->{last_poll} = time + 60*5;
105        # die("Twitter seems to be having problems.\n");
106        return;
107    }
108    if(exists($ratelimit->{remaining_hits})
109       && $ratelimit->{remaining_hits} <= 0) {
110        $self->{last_direct_poll} = $self->{last_poll} = $ratelimit->{reset_time_in_seconds};
111        die("Twitter: ratelimited until " . $ratelimit->{reset_time} . "\n");
112    } elsif(exists($ratelimit->{error})) {
113        die("Twitter: ". $ratelimit->{error} . "\n");
114        $self->{last_direct_poll} = $self->{last_poll} = time + 60*20;
115    }
116}
117
118sub poll_twitter {
119    my $self = shift;
120
121    return unless ( time - $self->{last_poll} ) >= 60;
122    $self->{last_poll} = time;
123    return unless BarnOwl::getvar('twitter:poll') eq 'on';
124
125    my $timeline = eval { $self->{twitter}->friends_timeline( { since_id => $self->{last_id} } ) };
126    warn "$@" if $@;
127    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
128        $self->twitter_error();
129        return;
130    };
131
132    if ($self->{cfg}->{show_mentions}) {
133        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
134        warn "$@" if $@;
135        unless (defined($mentions) && ref($mentions) eq 'ARRAY') {
136            $self->twitter_error();
137            return;
138        };
139        #combine, sort by id, and uniq
140        push @$timeline, @$mentions;
141        @$timeline = sort { $b->{id} <=> $a->{id} } @$timeline;
142        my $prev = { id => 0 };
143        @$timeline = grep($_->{id} != $prev->{id} && (($prev) = $_), @$timeline);
144    }
145
146    if ( scalar @$timeline ) {
147        for my $tweet ( reverse @$timeline ) {
148            if ( $tweet->{id} <= $self->{last_id} ) {
149                next;
150            }
151            my $msg = BarnOwl::Message->new(
152                type      => 'Twitter',
153                sender    => $tweet->{user}{screen_name},
154                recipient => $self->{cfg}->{user} || $self->{user},
155                direction => 'in',
156                source    => decode_entities($tweet->{source}),
157                location  => decode_entities($tweet->{user}{location}||""),
158                body      => decode_entities($tweet->{text}),
159                status_id => $tweet->{id},
160                service   => $self->{cfg}->{service},
161                account   => $self->{cfg}->{account_nickname},
162               );
163            BarnOwl::queue_message($msg);
164        }
165        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
166    } else {
167        # BarnOwl::message("No new tweets...");
168    }
169}
170
171sub poll_direct {
172    my $self = shift;
173
174    return unless ( time - $self->{last_direct_poll}) >= 120;
175    $self->{last_direct_poll} = time;
176    return unless BarnOwl::getvar('twitter:poll') eq 'on';
177
178    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
179    warn "$@" if $@;
180    unless(defined($direct) && ref($direct) eq 'ARRAY') {
181        $self->twitter_error();
182        return;
183    };
184    if ( scalar @$direct ) {
185        for my $tweet ( reverse @$direct ) {
186            if ( $tweet->{id} <= $self->{last_direct} ) {
187                next;
188            }
189            my $msg = BarnOwl::Message->new(
190                type      => 'Twitter',
191                sender    => $tweet->{sender}{screen_name},
192                recipient => $self->{cfg}->{user} || $self->{user},
193                direction => 'in',
194                location  => decode_entities($tweet->{sender}{location}||""),
195                body      => decode_entities($tweet->{text}),
196                isprivate => 'true',
197                service   => $self->{cfg}->{service},
198                account   => $self->{cfg}->{account_nickname},
199               );
200            BarnOwl::queue_message($msg);
201        }
202        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
203    } else {
204        # BarnOwl::message("No new tweets...");
205    }
206}
207
208sub twitter {
209    my $self = shift;
210
211    my $msg = shift;
212    my $reply_to = shift;
213
214    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
215        $self->twitter_direct($1, $2);
216    } elsif(defined $self->{twitter}) {
217        if(length($msg) > 140) {
218            die("Twitter: Message over 140 characters long.\n");
219        }
220        if(defined($reply_to)) {
221            $self->{twitter}->update({
222                status => $msg,
223                in_reply_to_status_id => $reply_to
224               });
225        } else {
226            $self->{twitter}->update($msg);
227        }
228    }
229}
230
231sub twitter_direct {
232    my $self = shift;
233
234    my $who = shift;
235    my $msg = shift;
236    if(defined $self->{twitter}) {
237        $self->{twitter}->new_direct_message({
238            user => $who,
239            text => $msg
240           });
241        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
242            my $tweet = BarnOwl::Message->new(
243                type      => 'Twitter',
244                sender    => $self->{cfg}->{user} || $self->{user},
245                recipient => $who, 
246                direction => 'out',
247                body      => $msg,
248                isprivate => 'true',
249                service   => $self->{cfg}->{service},
250               );
251            BarnOwl::queue_message($tweet);
252        }
253    }
254}
255
256sub twitter_atreply {
257    my $self = shift;
258
259    my $to  = shift;
260    my $id  = shift;
261    my $msg = shift;
262    if(defined($id)) {
263        $self->twitter("@".$to." ".$msg, $id);
264    } else {
265        $self->twitter("@".$to." ".$msg);
266    }
267}
268
269sub twitter_follow {
270    my $self = shift;
271
272    my $who = shift;
273
274    my $user = $self->{twitter}->create_friend($who);
275    # returns a string on error
276    if (defined $user && !ref $user) {
277        BarnOwl::message($user);
278    } else {
279        BarnOwl::message("Following " . $who);
280    }
281}
282
283sub twitter_unfollow {
284    my $self = shift;
285
286    my $who = shift;
287
288    my $user = $self->{twitter}->destroy_friend($who);
289    # returns a string on error
290    if (defined $user && !ref $user) {
291        BarnOwl::message($user);
292    } else {
293        BarnOwl::message("No longer following " . $who);
294    }
295}
296
297sub nickname {
298    my $self = shift;
299    return $self->{cfg}->{account_nickname};
300}
301
3021;
Note: See TracBrowser for help on using the repository browser.