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

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