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

release-1.10release-1.7release-1.8release-1.9
Last change on this file since f3e44eb was f3e44eb, checked in by Nelson Elhage <nelhage@mit.edu>, 14 years ago
Add a command to count characters. Includes an awful hack to correctly handle @-replies.
  • Property mode set to 100644
File size: 10.6 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;
[188b745]28
[f3e44eb]29our $prefix;
30
[d689fc7]31my $desc = <<'END_DESC';
[d1bb4f3]32BarnOwl::Module::Twitter will watch for authentic zephyrs to
33-c $twitter:class -i $twitter:instance -O $twitter:opcode
34from your sender and mirror them to Twitter.
35
36A value of '*' in any of these fields acts a wildcard, accepting
37messages with any value of that field.
38END_DESC
[d689fc7]39BarnOwl::new_variable_string(
40    'twitter:class',
41    {
[40c9dac]42        default     => $ENV{USER},
[d689fc7]43        summary     => 'Class to watch for Twitter messages',
44        description => $desc
45    }
46);
47BarnOwl::new_variable_string(
48    'twitter:instance',
49    {
[40c9dac]50        default => 'status',
[d689fc7]51        summary => 'Instance on twitter:class to watch for Twitter messages.',
52        description => $desc
53    }
54);
55BarnOwl::new_variable_string(
56    'twitter:opcode',
57    {
[40c9dac]58        default => 'twitter',
[d689fc7]59        summary => 'Opcode for zephyrs that will be sent as twitter updates',
60        description => $desc
61    }
62);
[e54f2fa]63
[927c186]64BarnOwl::new_variable_bool(
65    'twitter:poll',
66    {
67        default => 1,
68        summary => 'Poll Twitter for incoming messages',
69        description => "If set, will poll Twitter every minute for normal updates,\n"
70        . 'and every two minutes for direct message'
71     }
72 );
73
[159aaad]74sub fail {
75    my $msg = shift;
76    undef @twitter_handles;
77    BarnOwl::admin_message('Twitter Error', $msg);
78    die("Twitter Error: $msg\n");
79}
80
[d775050]81my $conffile = BarnOwl::get_config_dir() . "/twitter";
82open(my $fh, "<", "$conffile") || fail("Unable to read $conffile");
[159aaad]83my $raw_cfg = do {local $/; <$fh>};
[e54f2fa]84close($fh);
85eval {
[159aaad]86    $raw_cfg = from_json($raw_cfg);
[e54f2fa]87};
[9bedca0]88if($@) {
[159aaad]89    fail("Unable to parse $conffile: $@");
[e54f2fa]90}
91
[159aaad]92$raw_cfg = [$raw_cfg] unless UNIVERSAL::isa $raw_cfg, "ARRAY";
93
[e010ee0]94# Perform some sanity checking on the configuration.
95{
96    my %nicks;
97    my $default = 0;
98
99    for my $cfg (@$raw_cfg) {
100        if(! exists $cfg->{user}) {
101            fail("Account has no username set.");
102        }
103        my $user = $cfg->{user};
104        if(! exists $cfg->{password}) {
[2fa9f803]105            fail("Account $user has no password set.");
[e010ee0]106        }
107        if(@$raw_cfg > 1&&
108           !exists($cfg->{account_nickname}) ) {
109            fail("Account $user has no account_nickname set.");
110        }
111        if($cfg->{account_nickname}) {
112            if($nicks{$cfg->{account_nickname}}++) {
113                fail("Nickname " . $cfg->{account_nickname} . " specified more than once.");
114            }
115        }
116        if($cfg->{default} || $cfg->{default_sender}) {
117            if($default++) {
118                fail("Multiple accounts marked as 'default'.");
119            }
120        }
121    }
122}
123
[f0de278]124# If there is only a single account, make publish_tweets default to
125# true.
126if (scalar @$raw_cfg == 1 && !exists($raw_cfg->[0]{publish_tweets})) {
127    $raw_cfg->[0]{publish_tweets} = 1;
128}
129
[159aaad]130for my $cfg (@$raw_cfg) {
[9eb9479]131    my $twitter_args = { username   => $cfg->{user},
[159aaad]132                        password   => $cfg->{password},
133                        source     => 'barnowl', 
134                    };
135    if (defined $cfg->{service}) {
136        my $service = $cfg->{service};
137        $twitter_args->{apiurl} = $service;
138        my $apihost = $service;
139        $apihost =~ s/^\s*http:\/\///;
140        $apihost =~ s/\/.*$//;
141        $apihost .= ':80' unless $apihost =~ /:\d+$/;
142        $twitter_args->{apihost} = $cfg->{apihost} || $apihost;
143        my $apirealm = "Laconica API";
144        $twitter_args->{apirealm} = $cfg->{apirealm} || $apirealm;
145    } else {
146        $cfg->{service} = 'http://twitter.com';
147    }
[b56f2c3]148
[39dd366]149    my $twitter_handle;
[d748296]150    eval {
[39dd366]151         $twitter_handle = BarnOwl::Module::Twitter::Handle->new($cfg, %$twitter_args);
[d748296]152    };
[39dd366]153    if ($@) {
154        BarnOwl::error($@);
155        next;
156    }
157    push @twitter_handles, $twitter_handle;
[efcd223]158}
159
[385dd69]160$default_handle = first {$_->{cfg}->{default}} @twitter_handles;
[efcd223]161if (!$default_handle && @twitter_handles) {
162    $default_handle = $twitter_handles[0];
[b56f2c3]163}
164
[d1bb4f3]165sub match {
166    my $val = shift;
167    my $pat = shift;
168    return $pat eq "*" || ($val eq $pat);
169}
170
[e54f2fa]171sub handle_message {
172    my $m = shift;
[40c9dac]173    my ($class, $instance, $opcode) = map{BarnOwl::getvar("twitter:$_")} qw(class instance opcode);
[176434d]174    if($m->type eq 'zephyr'
175       && $m->sender eq BarnOwl::zephyr_getsender()
[d1bb4f3]176       && match($m->class, $class)
177       && match($m->instance, $instance)
178       && match($m->opcode, $opcode)
[e54f2fa]179       && $m->auth eq 'YES') {
[159aaad]180        for my $handle (@twitter_handles) {
[efcd223]181            $handle->twitter($m->body) if $handle->{cfg}->{publish_tweets};
[159aaad]182        }
[e54f2fa]183    }
184}
185
[4ae10de]186sub poll_messages {
187    # If we are reloaded into a barnowl with the old
188    # BarnOwl::Module::Twitter loaded, it still has a main loop hook
189    # that will call this function every second. If we just delete it,
190    # it will get the old version, which will call poll on each of our
191    # handles every second. However, they no longer include the time
192    # check, and so we will poll a handle every second until
193    # ratelimited.
194
195    # So we include an empty function here.
196}
197
[efcd223]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
[5214546]257BarnOwl::new_command( 'twitter-retweet' => sub { cmd_twitter_retweet(@_) },
258    {
259    summary     => 'Retweet the current Twitter message',
260    usage       => 'twitter-retweet [ACCOUNT]',
261    description => <<END_DESCRIPTION
262Retweet the current Twitter message using ACCOUNT (defaults to the
263account that received the tweet).
264END_DESCRIPTION
265    }
266);
267
[513da71]268BarnOwl::new_command( 'twitter-follow' => sub { cmd_twitter_follow(@_); },
269    {
270    summary     => 'Follow a user on Twitter',
271    usage       => 'twitter-follow USER [ACCOUNT]',
272    description => 'Follow USER on Twitter ACCOUNT (defaults to default_sender, or first service'
273    . "\nif no default is provided)"
[6babb75]274    }
275);
276
[513da71]277BarnOwl::new_command( 'twitter-unfollow' => sub { cmd_twitter_unfollow(@_); },
278    {
279    summary     => 'Stop following a user on Twitter',
280    usage       => 'twitter-unfollow USER [ACCOUNT]',
281    description => 'Stop following USER on Twitter ACCOUNT (defaults to default_sender, or first'
282    . "\nservice if no default is provided)"
283    }
284);
[6babb75]285
[f3e44eb]286BarnOwl::new_command('twitter-count-chars' => \&cmd_count_chars, {
287    summary     => 'Count the number of characters in the edit window',
288    usage       => 'twitter-count-chars',
289    description => <<END_DESCRIPTION
290Displays the number of characters entered in the edit window so far. Correctly
291takes into account any \@user prefix that will be added by the Twitter plugin.
292END_DESCRIPTION
293   });
294
295
[4cf4067]296sub cmd_twitter {
297    my $cmd = shift;
[159aaad]298    my $account = shift;
299    if (defined $account) {
300        if(@_) {
301            my $status = join(" ", @_);
302            twitter($account, $status);
303            return;
304        }
[4cf4067]305    }
[f3e44eb]306    undef $prefix;
[22fce654]307    BarnOwl::start_edit_win("What's happening?" . (defined $account ? " ($account)" : ""), sub{twitter($account, shift)});
[4cf4067]308}
309
[927c186]310sub cmd_twitter_direct {
311    my $cmd = shift;
312    my $user = shift;
313    die("Usage: $cmd USER\n") unless $user;
[8462b38]314    my $account = find_account_default(shift);
[f3e44eb]315    undef $prefix;
[8462b38]316    BarnOwl::start_edit_win("$cmd $user " . ($account->nickname||""),
317                            sub { $account->twitter_direct($user, shift) });
[927c186]318}
319
[6babb75]320sub cmd_twitter_atreply {
321    my $cmd  = shift;
[acdd52e]322    my $user = shift || die("Usage: $cmd USER [In-Reply-To ID]\n");
323    my $id   = shift;
[8462b38]324    my $account = find_account_default(shift);
325
[f3e44eb]326    $prefix = "@\$user ";
[8462b38]327    BarnOwl::start_edit_win("Reply to \@" . $user . ($account->nickname ? (" on " . $account->nickname) : ""),
328                            sub { $account->twitter_atreply($user, $id, shift) });
[6babb75]329}
330
[5214546]331sub cmd_twitter_retweet {
332    my $cmd = shift;
333    my $account = shift;
334    my $m = BarnOwl::getcurmsg();
335    if(!$m || $m->type ne 'Twitter') {
336        die("$cmd must be used with a Twitter message selected.\n");
337    }
338
339    $account = $m->account unless defined($account);
340    find_account($account)->twitter_retweet($m);
341}
342
[513da71]343sub cmd_twitter_follow {
344    my $cmd = shift;
345    my $user = shift;
346    die("Usage: $cmd USER\n") unless $user;
347    my $account = shift;
[8462b38]348    find_account_default($account)->twitter_follow($user);
[513da71]349}
350
351sub cmd_twitter_unfollow {
352    my $cmd = shift;
353    my $user = shift;
354    die("Usage: $cmd USER\n") unless $user;
355    my $account = shift;
[8462b38]356    find_account_default($account)->twitter_unfollow($user);
[513da71]357}
358
[f3e44eb]359use BarnOwl::Editwin qw(:all);
360sub cmd_count_chars {
361    my $cmd = shift;
362    my $text = save_excursion {
363        move_to_buffer_start();
364        set_mark();
365        move_to_buffer_end();
366        get_region();
367    };
368    my $len = length($text);
369    $len += length($prefix) if $prefix;
370    BarnOwl::message($len);
371    return $len;
372}
373
[f93b81b]374BarnOwl::filter(qw{twitter type ^twitter$});
[8618438]375
[e54f2fa]3761;
Note: See TracBrowser for help on using the repository browser.