Changeset 159aaad


Ignore:
Timestamp:
Jul 21, 2009, 9:27:32 PM (15 years ago)
Author:
Nelson Elhage <nelhage@mit.edu>
Branches:
master, release-1.10, release-1.7, release-1.8, release-1.9
Children:
7430aa4
Parents:
f93b81b
git-author:
Kevin Riggle <kevinr@free-dissociation.com> (07/21/09 21:27:02)
git-committer:
Nelson Elhage <nelhage@mit.edu> (07/21/09 21:27:32)
Message:
Multiple account support

Accounts are specified as a list of hashes in the ~/.owl/twitter file.

Adds 'poll_for_tweets', 'poll_for_dms', 'publish_tweets', 'default_sender',
and 'account_nickname' options to the twitter account hashes.  They do about
what they say on the tin.

Add arguments to :twitter, :twitter-direct, and :twitter-atreply to specify
the service to use (by nickname), with sane defaults, plus documentation.
Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • README

    r8496cc2 r159aaad  
    2525also need to set 'apihost' and 'apirealm'. See Net::Twitter or your
    2626blogging service's documentation for more information.
     27
     28Twitter.par also supports using multiple Twitter and Twitter-compatible
     29microblogging accounts from the same instance.  To enable this, add
     30additional hashes to your ~/.owl/twitter file (wrapped in a JSON list,
     31eg. [{"account_nickname":"twitter","user":"nelhage","password":"sekrit"},
     32{"account_nickname":"identica","service":"http://identi.ca/api",
     33"user":"nelhage","password":"sekriter"}]
     34
     35By default, public messages (excluding at-replies) are sent to all
     36accounts listed except those with publish_tweets set to false.
     37
     38There are several additional account-specific parameters that control the
     39behavior of Twitter.par when using multiple accounts:
     40
     41* account_nickname (string, required if multiple accounts are in use)
     42    Specify a short name by which you can refer to the account, eg.
     43    "identica" (eg. :twitter-direct nelhage identica would send a direct
     44    message to @nelhage on identi.ca from your account nicknamed "identica").
     45* default_sender (boolean, default false)
     46    If true, direct messages and at-replies you send without specifying an
     47    account will be sent using this account.  If no account has this parameter,
     48    such messages will be sent using the first account listed.
     49* poll_for_tweets (boolean, default true)
     50    If true, tweets sent by your friends on this account will be displayed
     51    in your BarnOwl message list.
     52* poll_for_dms (boolean, default true)
     53    If true, Direct Messages sent to you by your friends on this account
     54    will be displayed in your BarnOwl message list.
     55* publish_tweets (boolean, default true)
     56    If true, tweets you send without specifying an account (either with the
     57    :twitter command or mirrored from a zephyr class) will be published to
     58    this account.
  • lib/BarnOwl/Message/Twitter.pm

    ra0385ad3 r159aaad  
    1616sub subcontext {undef}
    1717sub service { return (shift->{"service"} || "http://twitter.com"); }
     18sub account { return shift->{"account"}; }
    1819sub long_sender {
    1920    my $self = shift;
     
    2829        return $self->replysendercmd;
    2930    } elsif(exists($self->{status_id})) {
    30         return 'twitter-atreply ' . $self->sender . " " . $self->{status_id};
     31        return 'twitter-atreply ' . $self->sender . " " . $self->{status_id} . " " . $self->account;
    3132    } else {
    32         return 'twitter-atreply ' . $self->sender;
     33        return 'twitter-atreply ' . $self->sender . " " . $self->account;
    3334    }
    3435}
     
    3637sub replysendercmd {
    3738    my $self = shift;
    38     return 'twitter-direct ' . $self->sender;
     39    return 'twitter-direct ' . $self->sender . " " . $self->account;
    3940}
    4041
  • lib/BarnOwl/Module/Twitter.pm

    rf93b81b r159aaad  
    1414package BarnOwl::Module::Twitter;
    1515
    16 our $VERSION = 0.1;
     16our $VERSION = 0.2;
    1717
    1818use Net::Twitter;
     
    2121use BarnOwl;
    2222use BarnOwl::Hooks;
    23 use BarnOwl::Message::Twitter;
    24 use HTML::Entities;
    25 
    26 our $twitter;
     23use BarnOwl::Module::Twitter::Handle;
     24
     25our @twitter_handles;
     26our $default_handle;
    2727my $user     = BarnOwl::zephyr_getsender();
    2828my ($class)  = ($user =~ /(^[^@]+)/);
     
    3030my $opcode   = "twitter";
    3131my $use_reply_to = 0;
    32 
    33 sub fail {
    34     my $msg = shift;
    35     undef $twitter;
    36     BarnOwl::admin_message('Twitter Error', $msg);
    37     die("Twitter Error: $msg\n");
    38 }
    39 
    40 if($Net::Twitter::VERSION >= 2.06) {
    41     $use_reply_to = 1;
    42 }
     32my $next_service_to_poll = 0;
    4333
    4434my $desc = <<'END_DESC';
     
    8575 );
    8676
     77sub fail {
     78    my $msg = shift;
     79    undef @twitter_handles;
     80    BarnOwl::admin_message('Twitter Error', $msg);
     81    die("Twitter Error: $msg\n");
     82}
     83
    8784my $conffile = BarnOwl::get_config_dir() . "/twitter";
    8885open(my $fh, "<", "$conffile") || fail("Unable to read $conffile");
    89 my $cfg = do {local $/; <$fh>};
     86my $raw_cfg = do {local $/; <$fh>};
    9087close($fh);
    9188eval {
    92     $cfg = from_json($cfg);
     89    $raw_cfg = from_json($raw_cfg);
    9390};
    9491if($@) {
    95     fail("Unable to parse ~/.owl/twitter: $@");
    96 }
    97 
    98 my $twitter_args = { username   => $cfg->{user} || $user,
    99                      password   => $cfg->{password},
    100                      source     => 'barnowl',
    101                    };
    102 if (defined $cfg->{service}) {
    103     my $service = $cfg->{service};
    104     $twitter_args->{apiurl} = $service;
    105     my $apihost = $service;
    106     $apihost =~ s/^\s*http:\/\///;
    107     $apihost =~ s/\/.*$//;
    108     $apihost .= ':80' unless $apihost =~ /:\d+$/;
    109     $twitter_args->{apihost} = $cfg->{apihost} || $apihost;
    110     my $apirealm = "Laconica API";
    111     $twitter_args->{apirealm} = $cfg->{apirealm} || $apirealm;
    112 }
    113 
    114 $twitter  = Net::Twitter->new(%$twitter_args);
    115 
    116 if(!defined($twitter->verify_credentials())) {
    117     fail("Invalid twitter credentials");
    118 }
    119 
    120 our $last_poll        = 0;
    121 our $last_direct_poll = 0;
    122 our $last_id          = undef;
    123 our $last_direct      = undef;
    124 
    125 unless(defined($last_id)) {
    126     eval {
    127         $last_id = $twitter->friends_timeline({count => 1})->[0]{id};
    128     };
    129     $last_id = 0 unless defined($last_id);
    130 }
    131 
    132 unless(defined($last_direct)) {
    133     eval {
    134         $last_direct = $twitter->direct_messages()->[0]{id};
    135     };
    136     $last_direct = 0 unless defined($last_direct);
    137 }
    138 
    139 eval {
    140     $twitter->{ua}->timeout(1);
    141 };
     92    fail("Unable to parse $conffile: $@");
     93}
     94
     95$raw_cfg = [$raw_cfg] unless UNIVERSAL::isa $raw_cfg, "ARRAY";
     96
     97for my $cfg (@$raw_cfg) {
     98    my $twitter_args = { username   => $cfg->{user} || $user,
     99                        password   => $cfg->{password},
     100                        source     => 'barnowl',
     101                    };
     102    if (defined $cfg->{service}) {
     103        my $service = $cfg->{service};
     104        $twitter_args->{apiurl} = $service;
     105        my $apihost = $service;
     106        $apihost =~ s/^\s*http:\/\///;
     107        $apihost =~ s/\/.*$//;
     108        $apihost .= ':80' unless $apihost =~ /:\d+$/;
     109        $twitter_args->{apihost} = $cfg->{apihost} || $apihost;
     110        my $apirealm = "Laconica API";
     111        $twitter_args->{apirealm} = $cfg->{apirealm} || $apirealm;
     112    } else {
     113        $cfg->{service} = 'http://twitter.com';
     114    }
     115
     116    my $twitter_handle = BarnOwl::Module::Twitter::Handle->new($cfg, %$twitter_args);
     117    push @twitter_handles, $twitter_handle;
     118    $default_handle = $twitter_handle if (!defined $twitter_handle && exists $cfg->{default_sender} && $cfg->{default_sender});
     119}
    142120
    143121sub match {
     
    155133       && match($m->opcode, $opcode)
    156134       && $m->auth eq 'YES') {
    157         twitter($m->body);
     135        for my $handle (@twitter_handles) {
     136            $handle->twitter($m->body);
     137        }
    158138    }
    159139}
    160140
    161141sub poll_messages {
    162     poll_twitter();
    163     poll_direct();
    164 }
    165 
    166 sub twitter_error {
    167     my $ratelimit = $twitter->rate_limit_status;
    168     unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
    169         # Twitter's just sucking, sleep for 5 minutes
    170         $last_direct_poll = $last_poll = time + 60*5;
    171         # die("Twitter seems to be having problems.\n");
    172         return;
    173     }
    174     if(exists($ratelimit->{remaining_hits})
    175        && $ratelimit->{remaining_hits} <= 0) {
    176         $last_direct_poll = $last_poll = $ratelimit->{reset_time_in_seconds};
    177         die("Twitter: ratelimited until " . $ratelimit->{reset_time} . "\n");
    178     } elsif(exists($ratelimit->{error})) {
    179         die("Twitter: ". $ratelimit->{error} . "\n");
    180         $last_direct_poll = $last_poll = time + 60*20;
    181     }
    182 }
    183 
    184 sub poll_twitter {
    185     return unless ( time - $last_poll ) >= 60;
    186     $last_poll = time;
    187     return unless BarnOwl::getvar('twitter:poll') eq 'on';
    188 
    189     my $timeline = $twitter->friends_timeline( { since_id => $last_id } );
    190     unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
    191         twitter_error();
    192         return;
    193     };
    194     if ( scalar @$timeline ) {
    195         for my $tweet ( reverse @$timeline ) {
    196             if ( $tweet->{id} <= $last_id ) {
    197                 next;
     142    my $handle = $twitter_handles[$next_service_to_poll];
     143    $next_service_to_poll = ($next_service_to_poll + 1) % scalar(@twitter_handles);
     144   
     145    $handle->poll_twitter() if (!exists $handle->{cfg}->{poll_for_tweets} || $handle->{cfg}->{poll_for_tweets});
     146    $handle->poll_direct() if (!exists $handle->{cfg}->{poll_for_dms} || $handle->{cfg}->{poll_for_dms});
     147}
     148
     149sub twitter {
     150    my $account = shift;
     151
     152    my $sent = 0;
     153    if (defined $account) {
     154        for my $handle (@twitter_handles) {
     155            if (defined $handle->{cfg}->{account_nickname} && $account eq $handle->{cfg}->{account_nickname}) {
     156                $handle->twitter(@_);
     157                $sent = 1;
     158                last;
    198159            }
    199             my $msg = BarnOwl::Message->new(
    200                 type      => 'Twitter',
    201                 sender    => $tweet->{user}{screen_name},
    202                 recipient => $cfg->{user} || $user,
    203                 direction => 'in',
    204                 source    => decode_entities($tweet->{source}),
    205                 location  => decode_entities($tweet->{user}{location}||""),
    206                 body      => decode_entities($tweet->{text}),
    207                 status_id => $tweet->{id},
    208                 service   => $cfg->{service},
    209                );
    210             BarnOwl::queue_message($msg);
    211         }
    212         $last_id = $timeline->[0]{id};
    213     } else {
    214         # BarnOwl::message("No new tweets...");
    215     }
    216 }
    217 
    218 sub poll_direct {
    219     return unless ( time - $last_direct_poll) >= 120;
    220     $last_direct_poll = time;
    221     return unless BarnOwl::getvar('twitter:poll') eq 'on';
    222 
    223     my $direct = $twitter->direct_messages( { since_id => $last_direct } );
    224     unless(defined($direct) && ref($direct) eq 'ARRAY') {
    225         twitter_error();
    226         return;
    227     };
    228     if ( scalar @$direct ) {
    229         for my $tweet ( reverse @$direct ) {
    230             if ( $tweet->{id} <= $last_direct ) {
    231                 next;
     160        }
     161        BarnOwl::message("No Twitter account named " . $account) unless $sent == 1
     162    }
     163    else {
     164        # broadcast
     165        for my $handle (@twitter_handles) {
     166            $handle->twitter(@_) if (!exists $handle->{cfg}->{publish_tweets} || $handle->{cfg}->{publish_tweets});
     167        }
     168    }
     169}
     170
     171sub twitter_direct {
     172    my $account = shift;
     173
     174    my $sent = 0;
     175    if (defined $account) {
     176        for my $handle (@twitter_handles) {
     177            if (defined $handle->{cfg}->{account_nickname} && $account eq $handle->{cfg}->{account_nickname}) {
     178                $handle->twitter_direct(@_);
     179                $sent = 1;
     180                last;
    232181            }
    233             my $msg = BarnOwl::Message->new(
    234                 type      => 'Twitter',
    235                 sender    => $tweet->{sender}{screen_name},
    236                 recipient => $cfg->{user} || $user,
    237                 direction => 'in',
    238                 location  => decode_entities($tweet->{sender}{location}||""),
    239                 body      => decode_entities($tweet->{text}),
    240                 isprivate => 'true',
    241                 service   => $cfg->{service},
    242                );
    243             BarnOwl::queue_message($msg);
    244         }
    245         $last_direct = $direct->[0]{id};
    246     } else {
    247         # BarnOwl::message("No new tweets...");
    248     }
    249 }
    250 
    251 sub twitter {
    252     my $msg = shift;
    253     my $reply_to = shift;
    254 
    255     if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
    256         twitter_direct($1, $2);
    257     } elsif(defined $twitter) {
    258         if($use_reply_to && defined($reply_to)) {
    259             $twitter->update({
    260                 status => $msg,
    261                 in_reply_to_status_id => $reply_to
    262                });
    263         } else {
    264             $twitter->update($msg);
    265         }
    266     }
    267 }
    268 
    269 sub twitter_direct {
    270     my $who = shift;
    271     my $msg = shift;
    272     if(defined $twitter) {
    273         $twitter->new_direct_message({
    274             user => $who,
    275             text => $msg
    276            });
    277         if(BarnOwl::getvar("displayoutgoing") eq 'on') {
    278             my $tweet = BarnOwl::Message->new(
    279                 type      => 'Twitter',
    280                 sender    => $cfg->{user} || $user,
    281                 recipient => $who,
    282                 direction => 'out',
    283                 body      => $msg,
    284                 isprivate => 'true',
    285                 service   => $cfg->{service},
    286                );
    287             BarnOwl::queue_message($tweet);
    288         }
     182        }
     183        BarnOwl::message("No Twitter account named " . $account) unless $sent == 1
     184    }
     185    elsif (defined $default_handle) {
     186        $default_handle->twitter_direct(@_);
     187    }
     188    else {
     189        $twitter_handles[0]->twitter_direct(@_);
    289190    }
    290191}
    291192
    292193sub twitter_atreply {
    293     my $to  = shift;
    294     my $id  = shift;
    295     my $msg = shift;
    296     if(defined($id)) {
    297         twitter("@".$to." ".$msg, $id);
    298     } else {
    299         twitter("@".$to." ".$msg);
     194    my $account = shift;
     195
     196    my $sent = 0;
     197    if (defined $account) {
     198        for my $handle (@twitter_handles) {
     199            if (defined $handle->{cfg}->{account_nickname} && $account eq $handle->{cfg}->{account_nickname}) {
     200                $handle->twitter_atreply(@_);
     201                $sent = 1;
     202                last;
     203            }
     204        }
     205        BarnOwl::message("No Twitter account named " . $account) unless $sent == 1
     206    }
     207    elsif (defined $default_handle) {
     208        $default_handle->twitter_atreply(@_);
     209    }
     210    else {
     211        $twitter_handles[0]->twitter_atreply(@_);
    300212    }
    301213}
     
    303215BarnOwl::new_command(twitter => \&cmd_twitter, {
    304216    summary     => 'Update Twitter from BarnOwl',
    305     usage       => 'twitter [message]',
    306     description => 'Update Twitter. If MESSAGE is provided, use it as your status.'
     217    usage       => 'twitter [ACCOUNT] [MESSAGE]',
     218    description => 'Update Twitter on ACCOUNT. If MESSAGE is provided, use it as your status.'
     219    . "\nIf no ACCOUNT is provided, update all services which have publishing enabled."
    307220    . "\nOtherwise, prompt for a status message to use."
    308221   });
     
    310223BarnOwl::new_command('twitter-direct' => \&cmd_twitter_direct, {
    311224    summary     => 'Send a Twitter direct message',
    312     usage       => 'twitter-direct USER',
    313     description => 'Send a Twitter Direct Message to USER'
     225    usage       => 'twitter-direct USER [ACCOUNT]',
     226    description => 'Send a Twitter Direct Message to USER on ACCOUNT (defaults to default_sender,'
     227    . "\nor first service if no default is provided)"
    314228   });
    315229
     
    317231    {
    318232    summary     => 'Send a Twitter @ message',
    319     usage       => 'twitter-atreply USER',
    320     description => 'Send a Twitter @reply Message to USER'
     233    usage       => 'twitter-atreply USER [ACCOUNT]',
     234    description => 'Send a Twitter @reply Message to USER on ACCOUNT (defaults to default_sender,'
     235    . "or first service if no default is provided)"
    321236    }
    322237);
     
    325240sub cmd_twitter {
    326241    my $cmd = shift;
    327     if(@_) {
    328         my $status = join(" ", @_);
    329         twitter($status);
    330     } else {
    331       BarnOwl::start_edit_win('What are you doing?', \&twitter);
    332     }
     242    my $account = shift;
     243    if (defined $account) {
     244        if(@_) {
     245            my $status = join(" ", @_);
     246            twitter($account, $status);
     247            return;
     248        }
     249    }
     250    BarnOwl::start_edit_win('What are you doing?' . (defined $account ? " ($account)" : ""), sub{twitter($account, shift)});
    333251}
    334252
     
    337255    my $user = shift;
    338256    die("Usage: $cmd USER\n") unless $user;
    339     BarnOwl::start_edit_win("$cmd $user", sub{twitter_direct($user, shift)});
     257    my $account = shift;
     258    BarnOwl::start_edit_win("$cmd $user" . (defined $account ? " $account" : ""), sub{twitter_direct($account, $user, shift)});
    340259}
    341260
     
    344263    my $user = shift || die("Usage: $cmd USER [In-Reply-To ID]\n");
    345264    my $id   = shift;
    346     BarnOwl::start_edit_win("Reply to \@" . $user, sub { twitter_atreply($user, $id, shift) });
     265    my $account = shift;
     266    BarnOwl::start_edit_win("Reply to \@" . $user . (defined $account ? " on $account" : ""), sub { twitter_atreply($account, $user, $id, shift) });
    347267}
    348268
Note: See TracChangeset for help on using the changeset viewer.