source: perl/modules/Twitter/lib/BarnOwl/Module/Twitter.pm @ 1fd469d4

release-1.10release-1.7release-1.8release-1.9
Last change on this file since 1fd469d4 was 47a5e46, checked in by Nelson Elhage <nelhage@mit.edu>, 14 years ago
Merge Twitter under perl/modules
  • Property mode set to 100644
File size: 11.1 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
16our $VERSION = 0.2;
17
18use JSON;
19use List::Util qw(first);
20
21use BarnOwl;
22use BarnOwl::Hooks;
23use BarnOwl::Module::Twitter::Handle;
24use BarnOwl::Module::Twitter::Completion;
25
26our @twitter_handles = ();
27our $default_handle = undef;
28
29our $prefix;
30
31my $desc = <<'END_DESC';
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
39BarnOwl::new_variable_string(
40    'twitter:class',
41    {
42        default     => $ENV{USER},
43        summary     => 'Class to watch for Twitter messages',
44        description => $desc
45    }
46);
47BarnOwl::new_variable_string(
48    'twitter:instance',
49    {
50        default => 'status',
51        summary => 'Instance on twitter:class to watch for Twitter messages.',
52        description => $desc
53    }
54);
55BarnOwl::new_variable_string(
56    'twitter:opcode',
57    {
58        default => 'twitter',
59        summary => 'Opcode for zephyrs that will be sent as twitter updates',
60        description => $desc
61    }
62);
63
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
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
81my $conffile = BarnOwl::get_config_dir() . "/twitter";
82
83if (open(my $fh, "<", "$conffile")) {
84    read_config($fh);
85    close($fh);
86}
87
88sub read_config {
89    my $fh = shift;
90
91    my $raw_cfg = do {local $/; <$fh>};
92    close($fh);
93    eval {
94        $raw_cfg = from_json($raw_cfg);
95    };
96    if($@) {
97        fail("Unable to parse $conffile: $@");
98    }
99
100    $raw_cfg = [$raw_cfg] unless UNIVERSAL::isa $raw_cfg, "ARRAY";
101
102    # Perform some sanity checking on the configuration.
103  {
104      my %nicks;
105      my $default = 0;
106
107      for my $cfg (@$raw_cfg) {
108          if(! exists $cfg->{user}) {
109              fail("Account has no username set.");
110          }
111          my $user = $cfg->{user};
112          if(! exists $cfg->{password}) {
113              fail("Account $user has no password set.");
114          }
115          if(@$raw_cfg > 1&&
116             !exists($cfg->{account_nickname}) ) {
117              fail("Account $user has no account_nickname set.");
118          }
119          if($cfg->{account_nickname}) {
120              if($nicks{$cfg->{account_nickname}}++) {
121                  fail("Nickname " . $cfg->{account_nickname} . " specified more than once.");
122              }
123          }
124          if($cfg->{default} || $cfg->{default_sender}) {
125              if($default++) {
126                  fail("Multiple accounts marked as 'default'.");
127              }
128          }
129      }
130  }
131
132    # If there is only a single account, make publish_tweets default to
133    # true.
134    if (scalar @$raw_cfg == 1 && !exists($raw_cfg->[0]{publish_tweets})) {
135        $raw_cfg->[0]{publish_tweets} = 1;
136    }
137
138    for my $cfg (@$raw_cfg) {
139        my $twitter_args = { username   => $cfg->{user},
140                             password   => $cfg->{password},
141                             source     => 'barnowl', 
142                         };
143        if (defined $cfg->{service}) {
144            my $service = $cfg->{service};
145            $twitter_args->{apiurl} = $service;
146            my $apihost = $service;
147            $apihost =~ s/^\s*http:\/\///;
148            $apihost =~ s/\/.*$//;
149            $apihost .= ':80' unless $apihost =~ /:\d+$/;
150            $twitter_args->{apihost} = $cfg->{apihost} || $apihost;
151            my $apirealm = "Laconica API";
152            $twitter_args->{apirealm} = $cfg->{apirealm} || $apirealm;
153        } else {
154            $cfg->{service} = 'http://twitter.com';
155        }
156
157        my $twitter_handle;
158        eval {
159            $twitter_handle = BarnOwl::Module::Twitter::Handle->new($cfg, %$twitter_args);
160        };
161        if ($@) {
162            BarnOwl::error($@);
163            next;
164        }
165        push @twitter_handles, $twitter_handle;
166    }
167
168    $default_handle = first {$_->{cfg}->{default}} @twitter_handles;
169    if (!$default_handle && @twitter_handles) {
170        $default_handle = $twitter_handles[0];
171    }
172
173}
174
175sub match {
176    my $val = shift;
177    my $pat = shift;
178    return $pat eq "*" || ($val eq $pat);
179}
180
181sub handle_message {
182    my $m = shift;
183    my ($class, $instance, $opcode) = map{BarnOwl::getvar("twitter:$_")} qw(class instance opcode);
184    if($m->type eq 'zephyr'
185       && $m->sender eq BarnOwl::zephyr_getsender()
186       && match($m->class, $class)
187       && match($m->instance, $instance)
188       && match($m->opcode, $opcode)
189       && $m->auth eq 'YES') {
190        for my $handle (@twitter_handles) {
191            $handle->twitter($m->body) if $handle->{cfg}->{publish_tweets};
192        }
193    }
194}
195
196sub poll_messages {
197    # If we are reloaded into a barnowl with the old
198    # BarnOwl::Module::Twitter loaded, it still has a main loop hook
199    # that will call this function every second. If we just delete it,
200    # it will get the old version, which will call poll on each of our
201    # handles every second. However, they no longer include the time
202    # check, and so we will poll a handle every second until
203    # ratelimited.
204
205    # So we include an empty function here.
206}
207
208sub find_account {
209    my $name = shift;
210    my $handle = first {$_->{cfg}->{account_nickname} eq $name} @twitter_handles;
211    if ($handle) {
212        return $handle;
213    } else {
214        die("No such Twitter account: $name\n");
215    }
216}
217
218sub find_account_default {
219    my $name = shift;
220    if(defined($name)) {
221        return find_account($name);
222    } else {
223        return $default_handle;
224    }
225}
226
227sub twitter {
228    my $account = shift;
229
230    my $sent = 0;
231    if (defined $account) {
232        my $handle = find_account($account);
233        $handle->twitter(@_);
234    } 
235    else {
236        # broadcast
237        for my $handle (@twitter_handles) {
238            $handle->twitter(@_) if $handle->{cfg}->{publish_tweets};
239        }
240    }
241}
242
243BarnOwl::new_command(twitter => \&cmd_twitter, {
244    summary     => 'Update Twitter from BarnOwl',
245    usage       => 'twitter [ACCOUNT] [MESSAGE]',
246    description => 'Update Twitter on ACCOUNT. If MESSAGE is provided, use it as your status.'
247    . "\nIf no ACCOUNT is provided, update all services which have publishing enabled."
248    . "\nOtherwise, prompt for a status message to use."
249   });
250
251BarnOwl::new_command('twitter-direct' => \&cmd_twitter_direct, {
252    summary     => 'Send a Twitter direct message',
253    usage       => 'twitter-direct USER [ACCOUNT]',
254    description => 'Send a Twitter Direct Message to USER on ACCOUNT (defaults to default_sender,'
255    . "\nor first service if no default is provided)"
256   });
257
258BarnOwl::new_command( 'twitter-atreply' => sub { cmd_twitter_atreply(@_); },
259    {
260    summary     => 'Send a Twitter @ message',
261    usage       => 'twitter-atreply USER [ACCOUNT]',
262    description => 'Send a Twitter @reply Message to USER on ACCOUNT (defaults to default_sender,' 
263    . "\nor first service if no default is provided)"
264    }
265);
266
267BarnOwl::new_command( 'twitter-retweet' => sub { cmd_twitter_retweet(@_) },
268    {
269    summary     => 'Retweet the current Twitter message',
270    usage       => 'twitter-retweet [ACCOUNT]',
271    description => <<END_DESCRIPTION
272Retweet the current Twitter message using ACCOUNT (defaults to the
273account that received the tweet).
274END_DESCRIPTION
275    }
276);
277
278BarnOwl::new_command( 'twitter-follow' => sub { cmd_twitter_follow(@_); },
279    {
280    summary     => 'Follow a user on Twitter',
281    usage       => 'twitter-follow USER [ACCOUNT]',
282    description => 'Follow USER on Twitter ACCOUNT (defaults to default_sender, or first service'
283    . "\nif no default is provided)"
284    }
285);
286
287BarnOwl::new_command( 'twitter-unfollow' => sub { cmd_twitter_unfollow(@_); },
288    {
289    summary     => 'Stop following a user on Twitter',
290    usage       => 'twitter-unfollow USER [ACCOUNT]',
291    description => 'Stop following USER on Twitter ACCOUNT (defaults to default_sender, or first'
292    . "\nservice if no default is provided)"
293    }
294);
295
296BarnOwl::new_command('twitter-count-chars' => \&cmd_count_chars, {
297    summary     => 'Count the number of characters in the edit window',
298    usage       => 'twitter-count-chars',
299    description => <<END_DESCRIPTION
300Displays the number of characters entered in the edit window so far. Correctly
301takes into account any \@user prefix that will be added by the Twitter plugin.
302END_DESCRIPTION
303   });
304
305
306sub cmd_twitter {
307    my $cmd = shift;
308    my $account = shift;
309    if (defined $account) {
310        if(@_) {
311            my $status = join(" ", @_);
312            twitter($account, $status);
313            return;
314        }
315    }
316    undef $prefix;
317    BarnOwl::start_edit_win("What's happening?" . (defined $account ? " ($account)" : ""), sub{twitter($account, shift)});
318}
319
320sub cmd_twitter_direct {
321    my $cmd = shift;
322    my $user = shift;
323    die("Usage: $cmd USER\n") unless $user;
324    my $account = find_account_default(shift);
325    undef $prefix;
326    BarnOwl::start_edit_win("$cmd $user " . ($account->nickname||""),
327                            sub { $account->twitter_direct($user, shift) });
328}
329
330sub cmd_twitter_atreply {
331    my $cmd  = shift;
332    my $user = shift || die("Usage: $cmd USER [In-Reply-To ID]\n");
333    my $id   = shift;
334    my $account = find_account_default(shift);
335
336    $prefix = "\@$user ";
337    BarnOwl::start_edit_win("Reply to \@" . $user . ($account->nickname ? (" on " . $account->nickname) : ""),
338                            sub { $account->twitter_atreply($user, $id, shift) });
339}
340
341sub cmd_twitter_retweet {
342    my $cmd = shift;
343    my $account = shift;
344    my $m = BarnOwl::getcurmsg();
345    if(!$m || $m->type ne 'Twitter') {
346        die("$cmd must be used with a Twitter message selected.\n");
347    }
348
349    $account = $m->account unless defined($account);
350    find_account($account)->twitter_retweet($m);
351    return;
352}
353
354sub cmd_twitter_follow {
355    my $cmd = shift;
356    my $user = shift;
357    die("Usage: $cmd USER\n") unless $user;
358    my $account = shift;
359    find_account_default($account)->twitter_follow($user);
360}
361
362sub cmd_twitter_unfollow {
363    my $cmd = shift;
364    my $user = shift;
365    die("Usage: $cmd USER\n") unless $user;
366    my $account = shift;
367    find_account_default($account)->twitter_unfollow($user);
368}
369
370use BarnOwl::Editwin qw(:all);
371sub cmd_count_chars {
372    my $cmd = shift;
373    my $text = save_excursion {
374        move_to_buffer_start();
375        set_mark();
376        move_to_buffer_end();
377        get_region();
378    };
379    my $len = length($text);
380    $len += length($prefix) if $prefix;
381    BarnOwl::message($len);
382    return $len;
383}
384
385eval {
386    $BarnOwl::Hooks::receiveMessage->add("BarnOwl::Module::Twitter::handle_message");
387};
388if($@) {
389    $BarnOwl::Hooks::receiveMessage->add(\&handle_message);
390}
391
392
393
394BarnOwl::filter(qw{twitter type ^twitter$});
395
3961;
Note: See TracBrowser for help on using the repository browser.