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

Last change on this file was 1f69b54, checked in by Alex Dehnert <adehnert@mit.edu>, 7 years ago
Implement Twitter at-reply prefill semantics. Signed-off-by: Edward Z. Yang <ezyang@cs.stanford.edu>
  • Property mode set to 100644
File size: 13.2 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 JSON;
[efcd223]19use List::Util qw(first);
[e54f2fa]20
21use BarnOwl;
22use BarnOwl::Hooks;
[159aaad]23use BarnOwl::Module::Twitter::Handle;
[6e8eb1c]24use BarnOwl::Module::Twitter::Completion;
[e54f2fa]25
[d748296]26our @twitter_handles = ();
27our $default_handle = undef;
[188b745]28
[d689fc7]29my $desc = <<'END_DESC';
[d1bb4f3]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
[d689fc7]37BarnOwl::new_variable_string(
38    'twitter:class',
39    {
[40c9dac]40        default     => $ENV{USER},
[d689fc7]41        summary     => 'Class to watch for Twitter messages',
42        description => $desc
43    }
44);
45BarnOwl::new_variable_string(
46    'twitter:instance',
47    {
[40c9dac]48        default => 'status',
[d689fc7]49        summary => 'Instance on twitter:class to watch for Twitter messages.',
50        description => $desc
51    }
52);
53BarnOwl::new_variable_string(
54    'twitter:opcode',
55    {
[40c9dac]56        default => 'twitter',
[d689fc7]57        summary => 'Opcode for zephyrs that will be sent as twitter updates',
58        description => $desc
59    }
60);
[e54f2fa]61
[927c186]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
[159aaad]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
[d775050]79my $conffile = BarnOwl::get_config_dir() . "/twitter";
[ab28a06]80
81if (open(my $fh, "<", "$conffile")) {
82    read_config($fh);
83    close($fh);
[e54f2fa]84}
85
[ab28a06]86sub read_config {
87    my $fh = shift;
[159aaad]88
[ab28a06]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    }
[e010ee0]97
[ab28a06]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;
[e010ee0]134    }
135
[ab28a06]136    for my $cfg (@$raw_cfg) {
137        my $twitter_args = { username   => $cfg->{user},
138                             password   => $cfg->{password},
[7ff3907]139                             source     => 'barnowl',
[c53f5e8]140                             ssl        => 1,
[7ff3907]141                             legacy_lists_api => 0,
[ab28a06]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        }
[f0de278]156
[ab28a06]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;
[159aaad]166    }
[b56f2c3]167
[ab28a06]168    $default_handle = first {$_->{cfg}->{default}} @twitter_handles;
169    if (!$default_handle && @twitter_handles) {
170        $default_handle = $twitter_handles[0];
[39dd366]171    }
[efcd223]172
[b56f2c3]173}
174
[d1bb4f3]175sub match {
176    my $val = shift;
177    my $pat = shift;
178    return $pat eq "*" || ($val eq $pat);
179}
180
[e54f2fa]181sub handle_message {
182    my $m = shift;
[40c9dac]183    my ($class, $instance, $opcode) = map{BarnOwl::getvar("twitter:$_")} qw(class instance opcode);
[176434d]184    if($m->type eq 'zephyr'
185       && $m->sender eq BarnOwl::zephyr_getsender()
[d1bb4f3]186       && match($m->class, $class)
187       && match($m->instance, $instance)
188       && match($m->opcode, $opcode)
[e54f2fa]189       && $m->auth eq 'YES') {
[159aaad]190        for my $handle (@twitter_handles) {
[efcd223]191            $handle->twitter($m->body) if $handle->{cfg}->{publish_tweets};
[159aaad]192        }
[e54f2fa]193    }
194}
195
[4ae10de]196sub poll_messages {
[b8a3e00]197    # If we are reloaded into a BarnOwl with the old
[4ae10de]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
[efcd223]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    }
[927c186]216}
217
[8462b38]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
[159aaad]227sub twitter {
228    my $account = shift;
229
230    my $sent = 0;
231    if (defined $account) {
[efcd223]232        my $handle = find_account($account);
233        $handle->twitter(@_);
[159aaad]234    } 
235    else {
236        # broadcast
237        for my $handle (@twitter_handles) {
[efcd223]238            $handle->twitter(@_) if $handle->{cfg}->{publish_tweets};
[159aaad]239        }
[8618438]240    }
241}
242
[4cf4067]243BarnOwl::new_command(twitter => \&cmd_twitter, {
244    summary     => 'Update Twitter from BarnOwl',
[38c584e]245    usage       => 'twitter [ACCOUNT [MESSAGE]]',
[159aaad]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."
[4cf4067]248    . "\nOtherwise, prompt for a status message to use."
249   });
250
[927c186]251BarnOwl::new_command('twitter-direct' => \&cmd_twitter_direct, {
252    summary     => 'Send a Twitter direct message',
[159aaad]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)"
[927c186]256   });
257
[6babb75]258BarnOwl::new_command( 'twitter-atreply' => sub { cmd_twitter_atreply(@_); },
259    {
260    summary     => 'Send a Twitter @ message',
[38c584e]261    usage       => 'twitter-atreply USER [ID [ACCOUNT]]',
[159aaad]262    description => 'Send a Twitter @reply Message to USER on ACCOUNT (defaults to default_sender,' 
[513da71]263    . "\nor first service if no default is provided)"
264    }
265);
266
[1f69b54]267BarnOwl::new_command( 'twitter-prefill' => sub { cmd_twitter_prefill(@_); },
268    {
269    summary     => 'Send a Twitter message with prefilled text',
270    usage       => 'twitter-prefill PREFILL [ID [ACCOUNT]]',
271    description => 'Send a Twitter message with initial text PREFILL on ACCOUNT (defaults to default_sender,' 
272    . "\nor first service if no default is provided)"
273    }
274);
275
[5214546]276BarnOwl::new_command( 'twitter-retweet' => sub { cmd_twitter_retweet(@_) },
277    {
278    summary     => 'Retweet the current Twitter message',
279    usage       => 'twitter-retweet [ACCOUNT]',
280    description => <<END_DESCRIPTION
281Retweet the current Twitter message using ACCOUNT (defaults to the
282account that received the tweet).
283END_DESCRIPTION
284    }
285);
286
[140429f]287BarnOwl::new_command( 'twitter-favorite' => sub { cmd_twitter_favorite(@_) },
288    {
289    summary     => 'Favorite the current Twitter message',
290    usage       => 'twitter-favorite [ACCOUNT]',
291    description => <<END_DESCRIPTION
292Favorite the current Twitter message using ACCOUNT (defaults to the
293account that received the tweet).
294END_DESCRIPTION
295    }
296);
297
[513da71]298BarnOwl::new_command( 'twitter-follow' => sub { cmd_twitter_follow(@_); },
299    {
300    summary     => 'Follow a user on Twitter',
301    usage       => 'twitter-follow USER [ACCOUNT]',
302    description => 'Follow USER on Twitter ACCOUNT (defaults to default_sender, or first service'
303    . "\nif no default is provided)"
[6babb75]304    }
305);
306
[513da71]307BarnOwl::new_command( 'twitter-unfollow' => sub { cmd_twitter_unfollow(@_); },
308    {
309    summary     => 'Stop following a user on Twitter',
310    usage       => 'twitter-unfollow USER [ACCOUNT]',
311    description => 'Stop following USER on Twitter ACCOUNT (defaults to default_sender, or first'
312    . "\nservice if no default is provided)"
313    }
314);
[6babb75]315
[f3e44eb]316BarnOwl::new_command('twitter-count-chars' => \&cmd_count_chars, {
317    summary     => 'Count the number of characters in the edit window',
318    usage       => 'twitter-count-chars',
319    description => <<END_DESCRIPTION
[a2640485]320Displays the number of characters entered in the edit window so far.
[f3e44eb]321END_DESCRIPTION
322   });
323
[f6e1262]324$BarnOwl::Hooks::getQuickstart->add( sub { twitter_quickstart(@_); } );
325
326sub twitter_quickstart {
327    return <<'EOF'
328@b[Twitter]:
329Add your Twitter account to ~/.owl/twitter, like:
330  {"user":"nelhage", "password":"sekrit"}
331Run :reload-module Twitter, then use :twitter to tweet.
332EOF
333}
334
[f3e44eb]335
[4cf4067]336sub cmd_twitter {
337    my $cmd = shift;
[159aaad]338    my $account = shift;
339    if (defined $account) {
340        if(@_) {
341            my $status = join(" ", @_);
342            twitter($account, $status);
343            return;
344        }
[4cf4067]345    }
[22fce654]346    BarnOwl::start_edit_win("What's happening?" . (defined $account ? " ($account)" : ""), sub{twitter($account, shift)});
[4cf4067]347}
348
[927c186]349sub cmd_twitter_direct {
350    my $cmd = shift;
351    my $user = shift;
352    die("Usage: $cmd USER\n") unless $user;
[8462b38]353    my $account = find_account_default(shift);
354    BarnOwl::start_edit_win("$cmd $user " . ($account->nickname||""),
355                            sub { $account->twitter_direct($user, shift) });
[927c186]356}
357
[6babb75]358sub cmd_twitter_atreply {
359    my $cmd  = shift;
[acdd52e]360    my $user = shift || die("Usage: $cmd USER [In-Reply-To ID]\n");
361    my $id   = shift;
[8462b38]362    my $account = find_account_default(shift);
363
364    BarnOwl::start_edit_win("Reply to \@" . $user . ($account->nickname ? (" on " . $account->nickname) : ""),
365                            sub { $account->twitter_atreply($user, $id, shift) });
[a2640485]366    BarnOwl::Editwin::insert_text("\@$user ");
[6babb75]367}
368
[1f69b54]369sub cmd_twitter_prefill {
370    my $cmd  = shift;
371    # prefill is responsible for spacing
372    my $prefill = shift || die("Usage: $cmd PREFILL [In-Reply-To ID]\n");
373    my $id   = shift;
374    my $account = find_account_default(shift);
375
376    my $msg = "What's happening?";
377    if ($id) {
378        # So, this isn't quite semantically correct, but it's close
379        # enough, and under the planned use-case, it will look identical.
380        $msg = "Reply to " . $prefill;
381    }
382    if ($account->nickname) {
383        # XXX formatting slightly suboptimal on What's happening message;
384        # and the behavior does not match up with 'twitter' anyhoo,
385        # which doesn't dispatch through account_find_default
386        $msg .= " on " . $account->nickname;
387    }
388    BarnOwl::start_edit_win($msg,
389                            sub { $account->twitter_atreply(undef, $id, shift) });
390    BarnOwl::Editwin::insert_text($prefill);
391}
392
[5214546]393sub cmd_twitter_retweet {
394    my $cmd = shift;
395    my $account = shift;
396    my $m = BarnOwl::getcurmsg();
397    if(!$m || $m->type ne 'Twitter') {
398        die("$cmd must be used with a Twitter message selected.\n");
399    }
400
401    $account = $m->account unless defined($account);
402    find_account($account)->twitter_retweet($m);
[986c9b1]403    return;
[5214546]404}
405
[140429f]406sub cmd_twitter_favorite {
407    my $cmd = shift;
408    my $account = shift;
409    my $m = BarnOwl::getcurmsg();
410    if(!$m || $m->type ne 'Twitter') {
411        die("$cmd must be used with a Twitter message selected.\n");
412    }
413
414    $account = $m->account unless defined($account);
415    find_account($account)->twitter_favorite($m);
416    return;
417}
418
[513da71]419sub cmd_twitter_follow {
420    my $cmd = shift;
421    my $user = shift;
422    die("Usage: $cmd USER\n") unless $user;
423    my $account = shift;
[8462b38]424    find_account_default($account)->twitter_follow($user);
[513da71]425}
426
427sub cmd_twitter_unfollow {
428    my $cmd = shift;
429    my $user = shift;
430    die("Usage: $cmd USER\n") unless $user;
431    my $account = shift;
[8462b38]432    find_account_default($account)->twitter_unfollow($user);
[513da71]433}
434
[f3e44eb]435use BarnOwl::Editwin qw(:all);
436sub cmd_count_chars {
437    my $cmd = shift;
438    my $text = save_excursion {
439        move_to_buffer_start();
440        set_mark();
441        move_to_buffer_end();
442        get_region();
443    };
444    my $len = length($text);
445    BarnOwl::message($len);
446    return $len;
447}
448
[6249a76d]449eval {
450    $BarnOwl::Hooks::receiveMessage->add("BarnOwl::Module::Twitter::handle_message");
451};
452if($@) {
453    $BarnOwl::Hooks::receiveMessage->add(\&handle_message);
454}
455
456
457
[f93b81b]458BarnOwl::filter(qw{twitter type ^twitter$});
[8618438]459
[e54f2fa]4601;
Note: See TracBrowser for help on using the repository browser.