source: lib/BarnOwl/Module/Twitter.pm @ 9eb9479

release-1.7release-1.8release-1.9
Last change on this file since 9eb9479 was 9eb9479, checked in by Nelson Elhage <nelhage@mit.edu>, 12 years ago
Don't rely on zephyr_getsender() at startup. With krb5 Zephyr, zephyr_getsender() is not guaranteed to work until Zephyr has been initialized. If we're seeing zephyrs, it's probably a safe bet to try calling it, but don't assume that it returns anything meaningful at startup.
  • Property mode set to 100644
File size: 9.4 KB
RevLine 
[e54f2fa]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
[159aaad]16our $VERSION = 0.2;
[af3415c]17
[e54f2fa]18use Net::Twitter;
19use JSON;
[efcd223]20use List::Util qw(first);
[e54f2fa]21
22use BarnOwl;
23use BarnOwl::Hooks;
[159aaad]24use BarnOwl::Module::Twitter::Handle;
[e54f2fa]25
[d748296]26our @twitter_handles = ();
27our $default_handle = undef;
[9eb9479]28my $class    = $ENV{USER};
[9bedca0]29my $instance = "status";
30my $opcode   = "twitter";
[d658c29]31my $use_reply_to = 0;
[159aaad]32my $next_service_to_poll = 0;
[188b745]33
[d689fc7]34my $desc = <<'END_DESC';
[d1bb4f3]35BarnOwl::Module::Twitter will watch for authentic zephyrs to
36-c $twitter:class -i $twitter:instance -O $twitter:opcode
37from your sender and mirror them to Twitter.
38
39A value of '*' in any of these fields acts a wildcard, accepting
40messages with any value of that field.
41END_DESC
[d689fc7]42BarnOwl::new_variable_string(
43    'twitter:class',
44    {
45        default     => $class,
46        summary     => 'Class to watch for Twitter messages',
47        description => $desc
48    }
49);
50BarnOwl::new_variable_string(
51    'twitter:instance',
52    {
53        default => $instance,
54        summary => 'Instance on twitter:class to watch for Twitter messages.',
55        description => $desc
56    }
57);
58BarnOwl::new_variable_string(
59    'twitter:opcode',
60    {
61        default => $opcode,
62        summary => 'Opcode for zephyrs that will be sent as twitter updates',
63        description => $desc
64    }
65);
[e54f2fa]66
[927c186]67BarnOwl::new_variable_bool(
68    'twitter:poll',
69    {
70        default => 1,
71        summary => 'Poll Twitter for incoming messages',
72        description => "If set, will poll Twitter every minute for normal updates,\n"
73        . 'and every two minutes for direct message'
74     }
75 );
76
[159aaad]77sub fail {
78    my $msg = shift;
79    undef @twitter_handles;
80    BarnOwl::admin_message('Twitter Error', $msg);
81    die("Twitter Error: $msg\n");
82}
83
[d775050]84my $conffile = BarnOwl::get_config_dir() . "/twitter";
85open(my $fh, "<", "$conffile") || fail("Unable to read $conffile");
[159aaad]86my $raw_cfg = do {local $/; <$fh>};
[e54f2fa]87close($fh);
88eval {
[159aaad]89    $raw_cfg = from_json($raw_cfg);
[e54f2fa]90};
[9bedca0]91if($@) {
[159aaad]92    fail("Unable to parse $conffile: $@");
[e54f2fa]93}
94
[159aaad]95$raw_cfg = [$raw_cfg] unless UNIVERSAL::isa $raw_cfg, "ARRAY";
96
[e010ee0]97# Perform some sanity checking on the configuration.
98{
99    my %nicks;
100    my $default = 0;
101
102    for my $cfg (@$raw_cfg) {
103        if(! exists $cfg->{user}) {
104            fail("Account has no username set.");
105        }
106        my $user = $cfg->{user};
107        if(! exists $cfg->{password}) {
[2fa9f803]108            fail("Account $user has no password set.");
[e010ee0]109        }
110        if(@$raw_cfg > 1&&
111           !exists($cfg->{account_nickname}) ) {
112            fail("Account $user has no account_nickname set.");
113        }
114        if($cfg->{account_nickname}) {
115            if($nicks{$cfg->{account_nickname}}++) {
116                fail("Nickname " . $cfg->{account_nickname} . " specified more than once.");
117            }
118        }
119        if($cfg->{default} || $cfg->{default_sender}) {
120            if($default++) {
121                fail("Multiple accounts marked as 'default'.");
122            }
123        }
124    }
125}
126
[f0de278]127# If there is only a single account, make publish_tweets default to
128# true.
129if (scalar @$raw_cfg == 1 && !exists($raw_cfg->[0]{publish_tweets})) {
130    $raw_cfg->[0]{publish_tweets} = 1;
131}
132
[159aaad]133for my $cfg (@$raw_cfg) {
[9eb9479]134    my $twitter_args = { username   => $cfg->{user},
[159aaad]135                        password   => $cfg->{password},
136                        source     => 'barnowl', 
137                    };
138    if (defined $cfg->{service}) {
139        my $service = $cfg->{service};
140        $twitter_args->{apiurl} = $service;
141        my $apihost = $service;
142        $apihost =~ s/^\s*http:\/\///;
143        $apihost =~ s/\/.*$//;
144        $apihost .= ':80' unless $apihost =~ /:\d+$/;
145        $twitter_args->{apihost} = $cfg->{apihost} || $apihost;
146        my $apirealm = "Laconica API";
147        $twitter_args->{apirealm} = $cfg->{apirealm} || $apirealm;
148    } else {
149        $cfg->{service} = 'http://twitter.com';
150    }
[b56f2c3]151
[39dd366]152    my $twitter_handle;
[d748296]153    eval {
[39dd366]154         $twitter_handle = BarnOwl::Module::Twitter::Handle->new($cfg, %$twitter_args);
[d748296]155    };
[39dd366]156    if ($@) {
157        BarnOwl::error($@);
158        next;
159    }
160    push @twitter_handles, $twitter_handle;
[efcd223]161}
162
[385dd69]163$default_handle = first {$_->{cfg}->{default}} @twitter_handles;
[efcd223]164if (!$default_handle && @twitter_handles) {
165    $default_handle = $twitter_handles[0];
[b56f2c3]166}
167
[d1bb4f3]168sub match {
169    my $val = shift;
170    my $pat = shift;
171    return $pat eq "*" || ($val eq $pat);
172}
173
[e54f2fa]174sub handle_message {
175    my $m = shift;
176    ($class, $instance, $opcode) = map{BarnOwl::getvar("twitter:$_")} qw(class instance opcode);
[9eb9479]177    if($m->sender eq BarnOwl::zephyr_getsender()
[d1bb4f3]178       && match($m->class, $class)
179       && match($m->instance, $instance)
180       && match($m->opcode, $opcode)
[e54f2fa]181       && $m->auth eq 'YES') {
[159aaad]182        for my $handle (@twitter_handles) {
[efcd223]183            $handle->twitter($m->body) if $handle->{cfg}->{publish_tweets};
[159aaad]184        }
[e54f2fa]185    }
186}
187
[8618438]188sub poll_messages {
[d748296]189    return unless @twitter_handles;
190
[159aaad]191    my $handle = $twitter_handles[$next_service_to_poll];
192    $next_service_to_poll = ($next_service_to_poll + 1) % scalar(@twitter_handles);
193   
[efcd223]194    $handle->poll_twitter() if $handle->{cfg}->{poll_for_tweets};
195    $handle->poll_direct() if $handle->{cfg}->{poll_for_dms};
196}
197
198sub find_account {
199    my $name = shift;
200    my $handle = first {$_->{cfg}->{account_nickname} eq $name} @twitter_handles;
201    if ($handle) {
202        return $handle;
203    } else {
204        die("No such Twitter account: $name\n");
205    }
[927c186]206}
207
[8462b38]208sub find_account_default {
209    my $name = shift;
210    if(defined($name)) {
211        return find_account($name);
212    } else {
213        return $default_handle;
214    }
215}
216
[159aaad]217sub twitter {
218    my $account = shift;
219
220    my $sent = 0;
221    if (defined $account) {
[efcd223]222        my $handle = find_account($account);
223        $handle->twitter(@_);
[159aaad]224    } 
225    else {
226        # broadcast
227        for my $handle (@twitter_handles) {
[efcd223]228            $handle->twitter(@_) if $handle->{cfg}->{publish_tweets};
[159aaad]229        }
[8618438]230    }
231}
232
[4cf4067]233BarnOwl::new_command(twitter => \&cmd_twitter, {
234    summary     => 'Update Twitter from BarnOwl',
[159aaad]235    usage       => 'twitter [ACCOUNT] [MESSAGE]',
236    description => 'Update Twitter on ACCOUNT. If MESSAGE is provided, use it as your status.'
237    . "\nIf no ACCOUNT is provided, update all services which have publishing enabled."
[4cf4067]238    . "\nOtherwise, prompt for a status message to use."
239   });
240
[927c186]241BarnOwl::new_command('twitter-direct' => \&cmd_twitter_direct, {
242    summary     => 'Send a Twitter direct message',
[159aaad]243    usage       => 'twitter-direct USER [ACCOUNT]',
244    description => 'Send a Twitter Direct Message to USER on ACCOUNT (defaults to default_sender,'
245    . "\nor first service if no default is provided)"
[927c186]246   });
247
[6babb75]248BarnOwl::new_command( 'twitter-atreply' => sub { cmd_twitter_atreply(@_); },
249    {
250    summary     => 'Send a Twitter @ message',
[159aaad]251    usage       => 'twitter-atreply USER [ACCOUNT]',
252    description => 'Send a Twitter @reply Message to USER on ACCOUNT (defaults to default_sender,' 
[513da71]253    . "\nor first service if no default is provided)"
254    }
255);
256
257BarnOwl::new_command( 'twitter-follow' => sub { cmd_twitter_follow(@_); },
258    {
259    summary     => 'Follow a user on Twitter',
260    usage       => 'twitter-follow USER [ACCOUNT]',
261    description => 'Follow USER on Twitter ACCOUNT (defaults to default_sender, or first service'
262    . "\nif no default is provided)"
[6babb75]263    }
264);
265
[513da71]266BarnOwl::new_command( 'twitter-unfollow' => sub { cmd_twitter_unfollow(@_); },
267    {
268    summary     => 'Stop following a user on Twitter',
269    usage       => 'twitter-unfollow USER [ACCOUNT]',
270    description => 'Stop following USER on Twitter ACCOUNT (defaults to default_sender, or first'
271    . "\nservice if no default is provided)"
272    }
273);
[6babb75]274
[4cf4067]275sub cmd_twitter {
276    my $cmd = shift;
[159aaad]277    my $account = shift;
278    if (defined $account) {
279        if(@_) {
280            my $status = join(" ", @_);
281            twitter($account, $status);
282            return;
283        }
[4cf4067]284    }
[159aaad]285    BarnOwl::start_edit_win('What are you doing?' . (defined $account ? " ($account)" : ""), sub{twitter($account, shift)});
[4cf4067]286}
287
[927c186]288sub cmd_twitter_direct {
289    my $cmd = shift;
290    my $user = shift;
291    die("Usage: $cmd USER\n") unless $user;
[8462b38]292    my $account = find_account_default(shift);
293    BarnOwl::start_edit_win("$cmd $user " . ($account->nickname||""),
294                            sub { $account->twitter_direct($user, shift) });
[927c186]295}
296
[6babb75]297sub cmd_twitter_atreply {
298    my $cmd  = shift;
[acdd52e]299    my $user = shift || die("Usage: $cmd USER [In-Reply-To ID]\n");
300    my $id   = shift;
[8462b38]301    my $account = find_account_default(shift);
302
303    BarnOwl::start_edit_win("Reply to \@" . $user . ($account->nickname ? (" on " . $account->nickname) : ""),
304                            sub { $account->twitter_atreply($user, $id, shift) });
[6babb75]305}
306
[513da71]307sub cmd_twitter_follow {
308    my $cmd = shift;
309    my $user = shift;
310    die("Usage: $cmd USER\n") unless $user;
311    my $account = shift;
[8462b38]312    find_account_default($account)->twitter_follow($user);
[513da71]313}
314
315sub cmd_twitter_unfollow {
316    my $cmd = shift;
317    my $user = shift;
318    die("Usage: $cmd USER\n") unless $user;
319    my $account = shift;
[8462b38]320    find_account_default($account)->twitter_unfollow($user);
[513da71]321}
322
[72b61dd]323eval {
324    $BarnOwl::Hooks::receiveMessage->add("BarnOwl::Module::Twitter::handle_message");
[8618438]325    $BarnOwl::Hooks::mainLoop->add("BarnOwl::Module::Twitter::poll_messages");
[72b61dd]326};
327if($@) {
328    $BarnOwl::Hooks::receiveMessage->add(\&handle_message);
[8618438]329    $BarnOwl::Hooks::mainLoop->add(\&poll_messages);
[72b61dd]330}
[e54f2fa]331
[f93b81b]332BarnOwl::filter(qw{twitter type ^twitter$});
[8618438]333
[e54f2fa]3341;
Note: See TracBrowser for help on using the repository browser.