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

Last change on this file since ba7ac0d was ba7ac0d, checked in by Edward Z. Yang <ezyang@cs.stanford.edu>, 8 years ago
Fix quoting conventions for Twitter commands. Two bugs here: 1. The documentation incorrectly implies that arguments can be omitted, when really the argument parser is much dumber than that. If there are two optional arguments, you can't omit the first one and give the second one; that's ambiguous. 2. There is a codepath for replycmd when status_id is not set (this should be impossible) which passed wrong arguments. Signed-off-by: Edward Z. Yang <ezyang@cs.stanford.edu>
  • Property mode set to 100644
File size: 12.0 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                             ssl        => 1,
141                             legacy_lists_api => 0,
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 [ID [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-favorite' => sub { cmd_twitter_favorite(@_) },
279    {
280    summary     => 'Favorite the current Twitter message',
281    usage       => 'twitter-favorite [ACCOUNT]',
282    description => <<END_DESCRIPTION
283Favorite the current Twitter message using ACCOUNT (defaults to the
284account that received the tweet).
285END_DESCRIPTION
286    }
287);
288
289BarnOwl::new_command( 'twitter-follow' => sub { cmd_twitter_follow(@_); },
290    {
291    summary     => 'Follow a user on Twitter',
292    usage       => 'twitter-follow USER [ACCOUNT]',
293    description => 'Follow USER on Twitter ACCOUNT (defaults to default_sender, or first service'
294    . "\nif no default is provided)"
295    }
296);
297
298BarnOwl::new_command( 'twitter-unfollow' => sub { cmd_twitter_unfollow(@_); },
299    {
300    summary     => 'Stop following a user on Twitter',
301    usage       => 'twitter-unfollow USER [ACCOUNT]',
302    description => 'Stop following USER on Twitter ACCOUNT (defaults to default_sender, or first'
303    . "\nservice if no default is provided)"
304    }
305);
306
307BarnOwl::new_command('twitter-count-chars' => \&cmd_count_chars, {
308    summary     => 'Count the number of characters in the edit window',
309    usage       => 'twitter-count-chars',
310    description => <<END_DESCRIPTION
311Displays the number of characters entered in the edit window so far.
312END_DESCRIPTION
313   });
314
315$BarnOwl::Hooks::getQuickstart->add( sub { twitter_quickstart(@_); } );
316
317sub twitter_quickstart {
318    return <<'EOF'
319@b[Twitter]:
320Add your Twitter account to ~/.owl/twitter, like:
321  {"user":"nelhage", "password":"sekrit"}
322Run :reload-module Twitter, then use :twitter to tweet.
323EOF
324}
325
326
327sub cmd_twitter {
328    my $cmd = shift;
329    my $account = shift;
330    if (defined $account) {
331        if(@_) {
332            my $status = join(" ", @_);
333            twitter($account, $status);
334            return;
335        }
336    }
337    BarnOwl::start_edit_win("What's happening?" . (defined $account ? " ($account)" : ""), sub{twitter($account, shift)});
338}
339
340sub cmd_twitter_direct {
341    my $cmd = shift;
342    my $user = shift;
343    die("Usage: $cmd USER\n") unless $user;
344    my $account = find_account_default(shift);
345    BarnOwl::start_edit_win("$cmd $user " . ($account->nickname||""),
346                            sub { $account->twitter_direct($user, shift) });
347}
348
349sub cmd_twitter_atreply {
350    my $cmd  = shift;
351    my $user = shift || die("Usage: $cmd USER [In-Reply-To ID]\n");
352    my $id   = shift;
353    my $account = find_account_default(shift);
354
355    BarnOwl::start_edit_win("Reply to \@" . $user . ($account->nickname ? (" on " . $account->nickname) : ""),
356                            sub { $account->twitter_atreply($user, $id, shift) });
357    BarnOwl::Editwin::insert_text("\@$user ");
358}
359
360sub cmd_twitter_retweet {
361    my $cmd = shift;
362    my $account = shift;
363    my $m = BarnOwl::getcurmsg();
364    if(!$m || $m->type ne 'Twitter') {
365        die("$cmd must be used with a Twitter message selected.\n");
366    }
367
368    $account = $m->account unless defined($account);
369    find_account($account)->twitter_retweet($m);
370    return;
371}
372
373sub cmd_twitter_favorite {
374    my $cmd = shift;
375    my $account = shift;
376    my $m = BarnOwl::getcurmsg();
377    if(!$m || $m->type ne 'Twitter') {
378        die("$cmd must be used with a Twitter message selected.\n");
379    }
380
381    $account = $m->account unless defined($account);
382    find_account($account)->twitter_favorite($m);
383    return;
384}
385
386sub cmd_twitter_follow {
387    my $cmd = shift;
388    my $user = shift;
389    die("Usage: $cmd USER\n") unless $user;
390    my $account = shift;
391    find_account_default($account)->twitter_follow($user);
392}
393
394sub cmd_twitter_unfollow {
395    my $cmd = shift;
396    my $user = shift;
397    die("Usage: $cmd USER\n") unless $user;
398    my $account = shift;
399    find_account_default($account)->twitter_unfollow($user);
400}
401
402use BarnOwl::Editwin qw(:all);
403sub cmd_count_chars {
404    my $cmd = shift;
405    my $text = save_excursion {
406        move_to_buffer_start();
407        set_mark();
408        move_to_buffer_end();
409        get_region();
410    };
411    my $len = length($text);
412    BarnOwl::message($len);
413    return $len;
414}
415
416eval {
417    $BarnOwl::Hooks::receiveMessage->add("BarnOwl::Module::Twitter::handle_message");
418};
419if($@) {
420    $BarnOwl::Hooks::receiveMessage->add(\&handle_message);
421}
422
423
424
425BarnOwl::filter(qw{twitter type ^twitter$});
426
4271;
Note: See TracBrowser for help on using the repository browser.