source: lib/BarnOwl/Module/Twitter.pm @ a0385ad3

release-1.7release-1.8release-1.9
Last change on this file since a0385ad3 was a0385ad3, checked in by Nelson Elhage <nelhage@mit.edu>, 13 years ago
Preliminary support for other Twitter-compatible microblogging services, eg. identi.ca. Adds an optional 'service' parameter to the configuration file, which points to the API root URL (defaults to Twitter's). Also adds optional 'apihost' and 'apirealm' parameters for the domain name and port of the site to connect to, and the name of the API realm, respectively (default to Twitters'; if service is not Twitter, attempt to guess sane defaults).
  • Property mode set to 100644
File size: 9.7 KB
Line 
1use warnings;
2use strict;
3
4=head1 NAME
5
6BarnOwl::Module::Twitter
7
8=head1 DESCRIPTION
9
10Post outgoing zephyrs from -c $USER -i status -O TWITTER to Twitter
11
12=cut
13
14package BarnOwl::Module::Twitter;
15
16use Net::Twitter;
17use JSON;
18
19use BarnOwl;
20use BarnOwl::Hooks;
21use BarnOwl::Message::Twitter;
22use HTML::Entities;
23
24our $twitter;
25my $user     = BarnOwl::zephyr_getsender();
26my ($class)  = ($user =~ /(^[^@]+)/);
27my $instance = "status";
28my $opcode   = "twitter";
29my $use_reply_to = 0;
30
31sub fail {
32    my $msg = shift;
33    undef $twitter;
34    BarnOwl::admin_message('Twitter Error', $msg);
35    die("Twitter Error: $msg\n");
36}
37
38if($Net::Twitter::VERSION >= 2.06) {
39    $use_reply_to = 1;
40}
41
42my $desc = <<'END_DESC';
43BarnOwl::Module::Twitter will watch for authentic zephyrs to
44-c $twitter:class -i $twitter:instance -O $twitter:opcode
45from your sender and mirror them to Twitter.
46
47A value of '*' in any of these fields acts a wildcard, accepting
48messages with any value of that field.
49END_DESC
50BarnOwl::new_variable_string(
51    'twitter:class',
52    {
53        default     => $class,
54        summary     => 'Class to watch for Twitter messages',
55        description => $desc
56    }
57);
58BarnOwl::new_variable_string(
59    'twitter:instance',
60    {
61        default => $instance,
62        summary => 'Instance on twitter:class to watch for Twitter messages.',
63        description => $desc
64    }
65);
66BarnOwl::new_variable_string(
67    'twitter:opcode',
68    {
69        default => $opcode,
70        summary => 'Opcode for zephyrs that will be sent as twitter updates',
71        description => $desc
72    }
73);
74
75BarnOwl::new_variable_bool(
76    'twitter:poll',
77    {
78        default => 1,
79        summary => 'Poll Twitter for incoming messages',
80        description => "If set, will poll Twitter every minute for normal updates,\n"
81        . 'and every two minutes for direct message'
82     }
83 );
84
85my $conffile = BarnOwl::get_config_dir() . "/twitter";
86open(my $fh, "<", "$conffile") || fail("Unable to read $conffile");
87my $cfg = do {local $/; <$fh>};
88close($fh);
89eval {
90    $cfg = from_json($cfg);
91};
92if($@) {
93    fail("Unable to parse ~/.owl/twitter: $@");
94}
95
96my $twitter_args = { username   => $cfg->{user} || $user,
97                     password   => $cfg->{password},
98                     source     => 'barnowl', 
99                   };
100if (defined $cfg->{service}) {
101    my $service = $cfg->{service};
102    $twitter_args->{apiurl} = $service;
103    my $apihost = $service;
104    $apihost =~ s/^\s*http:\/\///;
105    $apihost =~ s/\/.*$//;
106    $apihost .= ':80' unless $apihost =~ /:\d+$/;
107    $twitter_args->{apihost} = $cfg->{apihost} || $apihost;
108    my $apirealm = "Laconica API";
109    $twitter_args->{apirealm} = $cfg->{apirealm} || $apirealm;
110}
111
112$twitter  = Net::Twitter->new(%$twitter_args);
113
114if(!defined($twitter->verify_credentials())) {
115    fail("Invalid twitter credentials");
116}
117
118our $last_poll        = 0;
119our $last_direct_poll = 0;
120our $last_id          = undef;
121our $last_direct      = undef;
122
123unless(defined($last_id)) {
124    eval {
125        $last_id = $twitter->friends_timeline({count => 1})->[0]{id};
126    };
127    $last_id = 0 unless defined($last_id);
128}
129
130unless(defined($last_direct)) {
131    eval {
132        $last_direct = $twitter->direct_messages()->[0]{id};
133    };
134    $last_direct = 0 unless defined($last_direct);
135}
136
137eval {
138    $twitter->{ua}->timeout(1);
139};
140
141sub match {
142    my $val = shift;
143    my $pat = shift;
144    return $pat eq "*" || ($val eq $pat);
145}
146
147sub handle_message {
148    my $m = shift;
149    ($class, $instance, $opcode) = map{BarnOwl::getvar("twitter:$_")} qw(class instance opcode);
150    if($m->sender eq $user
151       && match($m->class, $class)
152       && match($m->instance, $instance)
153       && match($m->opcode, $opcode)
154       && $m->auth eq 'YES') {
155        twitter($m->body);
156    }
157}
158
159sub poll_messages {
160    poll_twitter();
161    poll_direct();
162}
163
164sub twitter_error {
165    my $ratelimit = $twitter->rate_limit_status;
166    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
167        # Twitter's just sucking, sleep for 5 minutes
168        $last_direct_poll = $last_poll = time + 60*5;
169        # die("Twitter seems to be having problems.\n");
170        return;
171    }
172    if(exists($ratelimit->{remaining_hits})
173       && $ratelimit->{remaining_hits} <= 0) {
174        $last_direct_poll = $last_poll = $ratelimit->{reset_time_in_seconds};
175        die("Twitter: ratelimited until " . $ratelimit->{reset_time} . "\n");
176    } elsif(exists($ratelimit->{error})) {
177        die("Twitter: ". $ratelimit->{error} . "\n");
178        $last_direct_poll = $last_poll = time + 60*20;
179    }
180}
181
182sub poll_twitter {
183    return unless ( time - $last_poll ) >= 60;
184    $last_poll = time;
185    return unless BarnOwl::getvar('twitter:poll') eq 'on';
186
187    my $timeline = $twitter->friends_timeline( { since_id => $last_id } );
188    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
189        twitter_error();
190        return;
191    };
192    if ( scalar @$timeline ) {
193        for my $tweet ( reverse @$timeline ) {
194            if ( $tweet->{id} <= $last_id ) {
195                next;
196            }
197            my $msg = BarnOwl::Message->new(
198                type      => 'Twitter',
199                sender    => $tweet->{user}{screen_name},
200                recipient => $cfg->{user} || $user,
201                direction => 'in',
202                source    => decode_entities($tweet->{source}),
203                location  => decode_entities($tweet->{user}{location}||""),
204                body      => decode_entities($tweet->{text}),
205                status_id => $tweet->{id},
206                service   => $cfg->{service},
207               );
208            BarnOwl::queue_message($msg);
209        }
210        $last_id = $timeline->[0]{id};
211    } else {
212        # BarnOwl::message("No new tweets...");
213    }
214}
215
216sub poll_direct {
217    return unless ( time - $last_direct_poll) >= 120;
218    $last_direct_poll = time;
219    return unless BarnOwl::getvar('twitter:poll') eq 'on';
220
221    my $direct = $twitter->direct_messages( { since_id => $last_direct } );
222    unless(defined($direct) && ref($direct) eq 'ARRAY') {
223        twitter_error();
224        return;
225    };
226    if ( scalar @$direct ) {
227        for my $tweet ( reverse @$direct ) {
228            if ( $tweet->{id} <= $last_direct ) {
229                next;
230            }
231            my $msg = BarnOwl::Message->new(
232                type      => 'Twitter',
233                sender    => $tweet->{sender}{screen_name},
234                recipient => $cfg->{user} || $user,
235                direction => 'in',
236                location  => decode_entities($tweet->{sender}{location}||""),
237                body      => decode_entities($tweet->{text}),
238                isprivate => 'true',
239                service   => $cfg->{service},
240               );
241            BarnOwl::queue_message($msg);
242        }
243        $last_direct = $direct->[0]{id};
244    } else {
245        # BarnOwl::message("No new tweets...");
246    }
247}
248
249sub twitter {
250    my $msg = shift;
251    my $reply_to = shift;
252
253    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
254        twitter_direct($1, $2);
255    } elsif(defined $twitter) {
256        if($use_reply_to && defined($reply_to)) {
257            $twitter->update({
258                status => $msg,
259                in_reply_to_status_id => $reply_to
260               });
261        } else {
262            $twitter->update($msg);
263        }
264    }
265}
266
267sub twitter_direct {
268    my $who = shift;
269    my $msg = shift;
270    if(defined $twitter) {
271        $twitter->new_direct_message({
272            user => $who,
273            text => $msg
274           });
275        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
276            my $tweet = BarnOwl::Message->new(
277                type      => 'Twitter',
278                sender    => $cfg->{user} || $user,
279                recipient => $who, 
280                direction => 'out',
281                body      => $msg,
282                isprivate => 'true',
283                service   => $cfg->{service},
284               );
285            BarnOwl::queue_message($tweet);
286        }
287    }
288}
289
290sub twitter_atreply {
291    my $to  = shift;
292    my $id  = shift;
293    my $msg = shift;
294    if(defined($id)) {
295        twitter("@".$to." ".$msg, $id);
296    } else {
297        twitter("@".$to." ".$msg);
298    }
299}
300
301BarnOwl::new_command(twitter => \&cmd_twitter, {
302    summary     => 'Update Twitter from BarnOwl',
303    usage       => 'twitter [message]',
304    description => 'Update Twitter. If MESSAGE is provided, use it as your status.'
305    . "\nOtherwise, prompt for a status message to use."
306   });
307
308BarnOwl::new_command('twitter-direct' => \&cmd_twitter_direct, {
309    summary     => 'Send a Twitter direct message',
310    usage       => 'twitter-direct USER',
311    description => 'Send a Twitter Direct Message to USER'
312   });
313
314BarnOwl::new_command( 'twitter-atreply' => sub { cmd_twitter_atreply(@_); },
315    {
316    summary     => 'Send a Twitter @ message',
317    usage       => 'twitter-atreply USER',
318    description => 'Send a Twitter @reply Message to USER'
319    }
320);
321
322
323sub cmd_twitter {
324    my $cmd = shift;
325    if(@_) {
326        my $status = join(" ", @_);
327        twitter($status);
328    } else {
329      BarnOwl::start_edit_win('What are you doing?', \&twitter);
330    }
331}
332
333sub cmd_twitter_direct {
334    my $cmd = shift;
335    my $user = shift;
336    die("Usage: $cmd USER\n") unless $user;
337    BarnOwl::start_edit_win("$cmd $user", sub{twitter_direct($user, shift)});
338}
339
340sub cmd_twitter_atreply {
341    my $cmd  = shift;
342    my $user = shift || die("Usage: $cmd USER [In-Reply-To ID]\n");
343    my $id   = shift;
344    BarnOwl::start_edit_win("Reply to \@" . $user, sub { twitter_atreply($user, $id, shift) });
345}
346
347eval {
348    $BarnOwl::Hooks::receiveMessage->add("BarnOwl::Module::Twitter::handle_message");
349    $BarnOwl::Hooks::mainLoop->add("BarnOwl::Module::Twitter::poll_messages");
350};
351if($@) {
352    $BarnOwl::Hooks::receiveMessage->add(\&handle_message);
353    $BarnOwl::Hooks::mainLoop->add(\&poll_messages);
354}
355
356BarnOwl::filter('twitter type ^twitter$');
357
3581;
Note: See TracBrowser for help on using the repository browser.