Index: perl/modules/Twitter/lib/BarnOwl/Message/Twitter.pm
===================================================================
--- perl/modules/Twitter/lib/BarnOwl/Message/Twitter.pm	(revision None)
+++ perl/modules/Twitter/lib/BarnOwl/Message/Twitter.pm	(revision None)
@@ -0,0 +1,63 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+BarnOwl::Message::Twitter
+
+=head1 DESCRIPTION
+
+=cut
+
+package BarnOwl::Message::Twitter;
+use base qw(BarnOwl::Message);
+
+sub context {'twitter'}
+sub subcontext {undef}
+sub service { return (shift->{"service"} || "http://twitter.com"); }
+sub account { return shift->{"account"}; }
+sub long_sender {
+    my $self = shift;
+    $self->service =~ m#^\s*(.*?://.*?)/.*$#;
+    my $service = $1 || $self->service;
+    return $service . '/' . $self->sender
+}
+
+sub replycmd {
+    my $self = shift;
+    if($self->is_private) {
+        return $self->replysendercmd;
+    } elsif(exists($self->{status_id})) {
+        return 'twitter-atreply ' . $self->sender . " " . $self->{status_id} . " " . $self->account;
+    } else {
+        return 'twitter-atreply ' . $self->sender . " " . $self->account;
+    }
+}
+
+sub replysendercmd {
+    my $self = shift;
+    return 'twitter-direct ' . $self->sender . " " . $self->account;
+}
+
+sub smartfilter {
+    my $self = shift;
+    my $inst = shift;
+    my $filter;
+
+    if($inst) {
+        $filter = "twitter-" . $self->sender;
+        BarnOwl::command("filter", $filter,
+                         qw{type ^twitter$ and sender}, '^'.$self->sender.'$');
+    } else {
+        $filter = "twitter";
+    }
+    return $filter;
+}
+
+=head1 SEE ALSO
+
+Foo, Bar, Baz
+
+=cut
+
+1;
Index: perl/modules/Twitter/lib/BarnOwl/Module/Twitter.pm
===================================================================
--- perl/modules/Twitter/lib/BarnOwl/Module/Twitter.pm	(revision None)
+++ perl/modules/Twitter/lib/BarnOwl/Module/Twitter.pm	(revision None)
@@ -0,0 +1,396 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+BarnOwl::Module::Twitter
+
+=head1 DESCRIPTION
+
+Post outgoing zephyrs from -c $USER -i status -O TWITTER to Twitter
+
+=cut
+
+package BarnOwl::Module::Twitter;
+
+our $VERSION = 0.2;
+
+use JSON;
+use List::Util qw(first);
+
+use BarnOwl;
+use BarnOwl::Hooks;
+use BarnOwl::Module::Twitter::Handle;
+use BarnOwl::Module::Twitter::Completion;
+
+our @twitter_handles = ();
+our $default_handle = undef;
+
+our $prefix;
+
+my $desc = <<'END_DESC';
+BarnOwl::Module::Twitter will watch for authentic zephyrs to
+-c $twitter:class -i $twitter:instance -O $twitter:opcode
+from your sender and mirror them to Twitter.
+
+A value of '*' in any of these fields acts a wildcard, accepting
+messages with any value of that field.
+END_DESC
+BarnOwl::new_variable_string(
+    'twitter:class',
+    {
+        default     => $ENV{USER},
+        summary     => 'Class to watch for Twitter messages',
+        description => $desc
+    }
+);
+BarnOwl::new_variable_string(
+    'twitter:instance',
+    {
+        default => 'status',
+        summary => 'Instance on twitter:class to watch for Twitter messages.',
+        description => $desc
+    }
+);
+BarnOwl::new_variable_string(
+    'twitter:opcode',
+    {
+        default => 'twitter',
+        summary => 'Opcode for zephyrs that will be sent as twitter updates',
+        description => $desc
+    }
+);
+
+BarnOwl::new_variable_bool(
+    'twitter:poll',
+    {
+        default => 1,
+        summary => 'Poll Twitter for incoming messages',
+        description => "If set, will poll Twitter every minute for normal updates,\n"
+        . 'and every two minutes for direct message'
+     }
+ );
+
+sub fail {
+    my $msg = shift;
+    undef @twitter_handles;
+    BarnOwl::admin_message('Twitter Error', $msg);
+    die("Twitter Error: $msg\n");
+}
+
+my $conffile = BarnOwl::get_config_dir() . "/twitter";
+
+if (open(my $fh, "<", "$conffile")) {
+    read_config($fh);
+    close($fh);
+}
+
+sub read_config {
+    my $fh = shift;
+
+    my $raw_cfg = do {local $/; <$fh>};
+    close($fh);
+    eval {
+        $raw_cfg = from_json($raw_cfg);
+    };
+    if($@) {
+        fail("Unable to parse $conffile: $@");
+    }
+
+    $raw_cfg = [$raw_cfg] unless UNIVERSAL::isa $raw_cfg, "ARRAY";
+
+    # Perform some sanity checking on the configuration.
+  {
+      my %nicks;
+      my $default = 0;
+
+      for my $cfg (@$raw_cfg) {
+          if(! exists $cfg->{user}) {
+              fail("Account has no username set.");
+          }
+          my $user = $cfg->{user};
+          if(! exists $cfg->{password}) {
+              fail("Account $user has no password set.");
+          }
+          if(@$raw_cfg > 1&&
+             !exists($cfg->{account_nickname}) ) {
+              fail("Account $user has no account_nickname set.");
+          }
+          if($cfg->{account_nickname}) {
+              if($nicks{$cfg->{account_nickname}}++) {
+                  fail("Nickname " . $cfg->{account_nickname} . " specified more than once.");
+              }
+          }
+          if($cfg->{default} || $cfg->{default_sender}) {
+              if($default++) {
+                  fail("Multiple accounts marked as 'default'.");
+              }
+          }
+      }
+  }
+
+    # If there is only a single account, make publish_tweets default to
+    # true.
+    if (scalar @$raw_cfg == 1 && !exists($raw_cfg->[0]{publish_tweets})) {
+        $raw_cfg->[0]{publish_tweets} = 1;
+    }
+
+    for my $cfg (@$raw_cfg) {
+        my $twitter_args = { username   => $cfg->{user},
+                             password   => $cfg->{password},
+                             source     => 'barnowl', 
+                         };
+        if (defined $cfg->{service}) {
+            my $service = $cfg->{service};
+            $twitter_args->{apiurl} = $service;
+            my $apihost = $service;
+            $apihost =~ s/^\s*http:\/\///;
+            $apihost =~ s/\/.*$//;
+            $apihost .= ':80' unless $apihost =~ /:\d+$/;
+            $twitter_args->{apihost} = $cfg->{apihost} || $apihost;
+            my $apirealm = "Laconica API";
+            $twitter_args->{apirealm} = $cfg->{apirealm} || $apirealm;
+        } else {
+            $cfg->{service} = 'http://twitter.com';
+        }
+
+        my $twitter_handle;
+        eval {
+            $twitter_handle = BarnOwl::Module::Twitter::Handle->new($cfg, %$twitter_args);
+        };
+        if ($@) {
+            BarnOwl::error($@);
+            next;
+        }
+        push @twitter_handles, $twitter_handle;
+    }
+
+    $default_handle = first {$_->{cfg}->{default}} @twitter_handles;
+    if (!$default_handle && @twitter_handles) {
+        $default_handle = $twitter_handles[0];
+    }
+
+}
+
+sub match {
+    my $val = shift;
+    my $pat = shift;
+    return $pat eq "*" || ($val eq $pat);
+}
+
+sub handle_message {
+    my $m = shift;
+    my ($class, $instance, $opcode) = map{BarnOwl::getvar("twitter:$_")} qw(class instance opcode);
+    if($m->type eq 'zephyr'
+       && $m->sender eq BarnOwl::zephyr_getsender()
+       && match($m->class, $class)
+       && match($m->instance, $instance)
+       && match($m->opcode, $opcode)
+       && $m->auth eq 'YES') {
+        for my $handle (@twitter_handles) {
+            $handle->twitter($m->body) if $handle->{cfg}->{publish_tweets};
+        }
+    }
+}
+
+sub poll_messages {
+    # If we are reloaded into a barnowl with the old
+    # BarnOwl::Module::Twitter loaded, it still has a main loop hook
+    # that will call this function every second. If we just delete it,
+    # it will get the old version, which will call poll on each of our
+    # handles every second. However, they no longer include the time
+    # check, and so we will poll a handle every second until
+    # ratelimited.
+
+    # So we include an empty function here.
+}
+
+sub find_account {
+    my $name = shift;
+    my $handle = first {$_->{cfg}->{account_nickname} eq $name} @twitter_handles;
+    if ($handle) {
+        return $handle;
+    } else {
+        die("No such Twitter account: $name\n");
+    }
+}
+
+sub find_account_default {
+    my $name = shift;
+    if(defined($name)) {
+        return find_account($name);
+    } else {
+        return $default_handle;
+    }
+}
+
+sub twitter {
+    my $account = shift;
+
+    my $sent = 0;
+    if (defined $account) {
+        my $handle = find_account($account);
+        $handle->twitter(@_);
+    } 
+    else {
+        # broadcast
+        for my $handle (@twitter_handles) {
+            $handle->twitter(@_) if $handle->{cfg}->{publish_tweets};
+        }
+    }
+}
+
+BarnOwl::new_command(twitter => \&cmd_twitter, {
+    summary     => 'Update Twitter from BarnOwl',
+    usage       => 'twitter [ACCOUNT] [MESSAGE]',
+    description => 'Update Twitter on ACCOUNT. If MESSAGE is provided, use it as your status.'
+    . "\nIf no ACCOUNT is provided, update all services which have publishing enabled."
+    . "\nOtherwise, prompt for a status message to use."
+   });
+
+BarnOwl::new_command('twitter-direct' => \&cmd_twitter_direct, {
+    summary     => 'Send a Twitter direct message',
+    usage       => 'twitter-direct USER [ACCOUNT]',
+    description => 'Send a Twitter Direct Message to USER on ACCOUNT (defaults to default_sender,'
+    . "\nor first service if no default is provided)"
+   });
+
+BarnOwl::new_command( 'twitter-atreply' => sub { cmd_twitter_atreply(@_); },
+    {
+    summary     => 'Send a Twitter @ message',
+    usage       => 'twitter-atreply USER [ACCOUNT]',
+    description => 'Send a Twitter @reply Message to USER on ACCOUNT (defaults to default_sender,' 
+    . "\nor first service if no default is provided)"
+    }
+);
+
+BarnOwl::new_command( 'twitter-retweet' => sub { cmd_twitter_retweet(@_) },
+    {
+    summary     => 'Retweet the current Twitter message',
+    usage       => 'twitter-retweet [ACCOUNT]',
+    description => <<END_DESCRIPTION
+Retweet the current Twitter message using ACCOUNT (defaults to the
+account that received the tweet).
+END_DESCRIPTION
+    }
+);
+
+BarnOwl::new_command( 'twitter-follow' => sub { cmd_twitter_follow(@_); },
+    {
+    summary     => 'Follow a user on Twitter',
+    usage       => 'twitter-follow USER [ACCOUNT]',
+    description => 'Follow USER on Twitter ACCOUNT (defaults to default_sender, or first service'
+    . "\nif no default is provided)"
+    }
+);
+
+BarnOwl::new_command( 'twitter-unfollow' => sub { cmd_twitter_unfollow(@_); },
+    {
+    summary     => 'Stop following a user on Twitter',
+    usage       => 'twitter-unfollow USER [ACCOUNT]',
+    description => 'Stop following USER on Twitter ACCOUNT (defaults to default_sender, or first'
+    . "\nservice if no default is provided)"
+    }
+);
+
+BarnOwl::new_command('twitter-count-chars' => \&cmd_count_chars, {
+    summary     => 'Count the number of characters in the edit window',
+    usage       => 'twitter-count-chars',
+    description => <<END_DESCRIPTION
+Displays the number of characters entered in the edit window so far. Correctly
+takes into account any \@user prefix that will be added by the Twitter plugin.
+END_DESCRIPTION
+   });
+
+
+sub cmd_twitter {
+    my $cmd = shift;
+    my $account = shift;
+    if (defined $account) {
+        if(@_) {
+            my $status = join(" ", @_);
+            twitter($account, $status);
+            return;
+        }
+    }
+    undef $prefix;
+    BarnOwl::start_edit_win("What's happening?" . (defined $account ? " ($account)" : ""), sub{twitter($account, shift)});
+}
+
+sub cmd_twitter_direct {
+    my $cmd = shift;
+    my $user = shift;
+    die("Usage: $cmd USER\n") unless $user;
+    my $account = find_account_default(shift);
+    undef $prefix;
+    BarnOwl::start_edit_win("$cmd $user " . ($account->nickname||""),
+                            sub { $account->twitter_direct($user, shift) });
+}
+
+sub cmd_twitter_atreply {
+    my $cmd  = shift;
+    my $user = shift || die("Usage: $cmd USER [In-Reply-To ID]\n");
+    my $id   = shift;
+    my $account = find_account_default(shift);
+
+    $prefix = "\@$user ";
+    BarnOwl::start_edit_win("Reply to \@" . $user . ($account->nickname ? (" on " . $account->nickname) : ""),
+                            sub { $account->twitter_atreply($user, $id, shift) });
+}
+
+sub cmd_twitter_retweet {
+    my $cmd = shift;
+    my $account = shift;
+    my $m = BarnOwl::getcurmsg();
+    if(!$m || $m->type ne 'Twitter') {
+        die("$cmd must be used with a Twitter message selected.\n");
+    }
+
+    $account = $m->account unless defined($account);
+    find_account($account)->twitter_retweet($m);
+    return;
+}
+
+sub cmd_twitter_follow {
+    my $cmd = shift;
+    my $user = shift;
+    die("Usage: $cmd USER\n") unless $user;
+    my $account = shift;
+    find_account_default($account)->twitter_follow($user);
+}
+
+sub cmd_twitter_unfollow {
+    my $cmd = shift;
+    my $user = shift;
+    die("Usage: $cmd USER\n") unless $user;
+    my $account = shift;
+    find_account_default($account)->twitter_unfollow($user);
+}
+
+use BarnOwl::Editwin qw(:all);
+sub cmd_count_chars {
+    my $cmd = shift;
+    my $text = save_excursion {
+        move_to_buffer_start();
+        set_mark();
+        move_to_buffer_end();
+        get_region();
+    };
+    my $len = length($text);
+    $len += length($prefix) if $prefix;
+    BarnOwl::message($len);
+    return $len;
+}
+
+eval {
+    $BarnOwl::Hooks::receiveMessage->add("BarnOwl::Module::Twitter::handle_message");
+};
+if($@) {
+    $BarnOwl::Hooks::receiveMessage->add(\&handle_message);
+}
+
+
+
+BarnOwl::filter(qw{twitter type ^twitter$});
+
+1;
Index: perl/modules/Twitter/lib/BarnOwl/Module/Twitter/Completion.pm
===================================================================
--- perl/modules/Twitter/lib/BarnOwl/Module/Twitter/Completion.pm	(revision None)
+++ perl/modules/Twitter/lib/BarnOwl/Module/Twitter/Completion.pm	(revision None)
@@ -0,0 +1,57 @@
+use strict;
+use warnings;
+
+package BarnOwl::Module::Twitter::Completion;
+
+my %users;
+
+sub complete_account {
+    my @nicks = grep {defined} map {$_->nickname} @BarnOwl::Module::Twitter::twitter_handles;
+}
+
+sub complete_user {
+    my $service = shift;
+    if(!$service) {
+        return map {keys %$_} values %users;
+    }
+    return keys %{$users{$service} || {}};
+}
+
+sub on_message {
+    my $m = shift;
+    return unless $m->type eq 'Twitter';
+
+    $users{$m->service}{$m->recipient} = 1;
+    $users{$m->service}{$m->sender} = 1;
+}
+
+sub complete_twitter {
+    my $ctx = shift;
+    return unless $ctx->word == 1;
+    return complete_account();
+}
+
+sub complete_twitter_user_account {
+    my $ctx = shift;
+    if ($ctx->word == 1) {
+        my $nick = $ctx->words->[2];
+        if ($nick) {
+            my  @completions = eval {
+                complete_user(BarnOwl::Module::Twitter::find_account($nick)->{cfg}{service});
+            };
+            return @completions if (!$@);
+        }
+        return complete_user();
+    } elsif ($ctx->word == 2) {
+        return complete_account();
+    }
+}
+
+BarnOwl::Completion::register_completer('twitter' => \&complete_twitter);
+BarnOwl::Completion::register_completer('twitter-direct' => \&complete_twitter_user_account);
+BarnOwl::Completion::register_completer('twitter-atreply' => \&complete_twitter_user_account);
+BarnOwl::Completion::register_completer('twitter-unfollow' => \&complete_twitter_user_account);
+
+$BarnOwl::Hooks::newMessage->add("BarnOwl::Module::Twitter::Completion::on_message");
+
+1;
Index: perl/modules/Twitter/lib/BarnOwl/Module/Twitter/Handle.pm
===================================================================
--- perl/modules/Twitter/lib/BarnOwl/Module/Twitter/Handle.pm	(revision None)
+++ perl/modules/Twitter/lib/BarnOwl/Module/Twitter/Handle.pm	(revision None)
@@ -0,0 +1,424 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+BarnOwl::Module::Twitter::Handle
+
+=head1 DESCRIPTION
+
+Contains everything needed to send and receive messages from a Twitter-like service.
+
+=cut
+
+package BarnOwl::Module::Twitter::Handle;
+
+use Net::Twitter::Lite;
+BEGIN {
+    # Backwards compatibility with version of Net::Twitter::Lite that
+    # lack home_timeline.
+    if(!defined(*Net::Twitter::Lite::home_timeline{CODE})) {
+        *Net::Twitter::Lite::home_timeline =
+          \&Net::Twitter::Lite::friends_timeline;
+    }
+}
+use HTML::Entities;
+
+use Scalar::Util qw(weaken);
+
+use BarnOwl;
+use BarnOwl::Message::Twitter;
+use POSIX qw(asctime);
+
+use constant BARNOWL_CONSUMER_KEY    => "9Py27vCQl6uB5V7ijmp31A";
+use constant BARNOWL_CONSUMER_SECRET => "GLhheSim8P5cVuk9FTM99KTEgWLW0LGl7gf54QWfg";
+
+sub fail {
+    my $self = shift;
+    my $msg = shift;
+    undef $self->{twitter};
+    my $nickname = $self->{cfg}->{account_nickname} || "";
+    die("[Twitter $nickname] Error: $msg\n");
+}
+
+sub new {
+    my $class = shift;
+    my $cfg = shift;
+
+    my $val;
+
+    if(!exists $cfg->{default} &&
+       defined($val = delete $cfg->{default_sender})) {
+        $cfg->{default} = $val;
+    }
+
+    if(!exists $cfg->{show_mentions} &&
+       defined($val = delete $cfg->{show_unsubscribed_replies})) {
+        $cfg->{show_mentions} = $val;
+    }
+
+    $cfg = {
+        account_nickname => '',
+        default          => 0,
+        poll_for_tweets  => 1,
+        poll_for_dms     => 1,
+        publish_tweets   => 0,
+        show_mentions    => 1,
+        %$cfg
+       };
+
+    my $self = {
+        'cfg'  => $cfg,
+        'twitter' => undef,
+        'last_id' => undef,
+        'last_direct' => undef,
+        'timer'        => undef,
+        'direct_timer' => undef
+    };
+
+    bless($self, $class);
+
+    my %twitter_args = @_;
+
+    my ($username, $password, $xauth);
+
+    if (*Net::Twitter::Lite::xauth{CODE}) {
+        $xauth = 1;
+        $username = delete $twitter_args{username};
+        $password = delete $twitter_args{password};
+        $twitter_args{consumer_key}    = BARNOWL_CONSUMER_KEY;
+        $twitter_args{consumer_secret} = BARNOWL_CONSUMER_SECRET;
+    } else {
+        BarnOwl::error("Please upgrade your version of Net::Twitter::Lite to support xAuth.");
+    }
+
+    $self->{twitter}  = Net::Twitter::Lite->new(%twitter_args,);
+
+    if ($xauth){
+        eval {
+            $self->{twitter}->xauth($username, $password);
+        };
+        if($@) {
+            $self->fail("Invalid credentials: $@");
+        }
+    }
+
+    my $timeline = eval { $self->{twitter}->home_timeline({count => 1}) };
+    warn "$@\n" if $@;
+
+    if(!defined($timeline) && !$xauth) {
+        $self->fail("Invalid credentials");
+    }
+
+    eval {
+        $self->{last_id} = $timeline->[0]{id};
+    };
+    $self->{last_id} = 1 unless defined($self->{last_id});
+
+    eval {
+        $self->{last_direct} = $self->{twitter}->direct_messages()->[0]{id};
+    };
+    warn "$@\n" if $@;
+    $self->{last_direct} = 1 unless defined($self->{last_direct});
+
+    eval {
+        $self->{twitter}->{ua}->timeout(1);
+    };
+    warn "$@\n" if $@;
+
+    $self->sleep(0);
+
+    return $self;
+}
+
+=head2 sleep SECONDS
+
+Stop polling Twitter for SECONDS seconds.
+
+=cut
+
+sub sleep {
+    my $self  = shift;
+    my $delay = shift;
+
+    my $weak = $self;
+    weaken($weak);
+
+    if($self->{cfg}->{poll_for_tweets}) {
+        $self->{timer} = BarnOwl::Timer->new({
+            after    => $delay,
+            interval => 90,
+            cb       => sub { $weak->poll_twitter if $weak }
+           });
+    }
+
+    if($self->{cfg}->{poll_for_dms}) {
+        $self->{direct_timer} = BarnOwl::Timer->new({
+            after    => $delay,
+            interval => 180,
+            cb       => sub { $weak->poll_direct if $weak }
+           });
+    }
+}
+
+=head2 twitter_command COMMAND ARGS...
+
+Call the specified method on $self->{twitter} with an extended
+timeout. This is intended for interactive commands, with the theory
+that if the user explicitly requested an action, it is slightly more
+acceptable to hang the UI for a second or two than to fail just
+because Twitter is being slightly slow. Polling commands should be
+called normally, with the default (short) timeout, to prevent
+background Twitter suckage from hosing the UI normally.
+
+=cut
+
+sub twitter_command {
+    my $self = shift;
+    my $cmd = shift;
+
+    eval { $self->{twitter}->{ua}->timeout(5); };
+    my $result = eval {
+        $self->{twitter}->$cmd(@_);
+    };
+    my $error = $@;
+    eval { $self->{twitter}->{ua}->timeout(1); };
+    if ($error) {
+        die($error);
+    }
+    return $result;
+}
+
+sub twitter_error {
+    my $self = shift;
+
+    my $ratelimit = eval { $self->{twitter}->rate_limit_status };
+    BarnOwl::debug($@) if $@;
+    unless(defined($ratelimit) && ref($ratelimit) eq 'HASH') {
+        # Twitter's probably just sucking, try again later.
+        $self->sleep(5*60);
+        return;
+    }
+
+    if(exists($ratelimit->{remaining_hits})
+       && $ratelimit->{remaining_hits} <= 0) {
+        my $timeout = $ratelimit->{reset_time_in_seconds};
+        $self->sleep($timeout - time + 60);
+        BarnOwl::error("Twitter" .
+                       ($self->{cfg}->{account_nickname} ?
+                        "[$self->{cfg}->{account_nickname}]" : "") .
+                        ": ratelimited until " . asctime(localtime($timeout)));
+    } elsif(exists($ratelimit->{error})) {
+        $self->sleep(60*20);
+        die("Twitter: ". $ratelimit->{error} . "\n");
+    }
+}
+
+sub poll_twitter {
+    my $self = shift;
+
+    return unless BarnOwl::getvar('twitter:poll') eq 'on';
+
+    BarnOwl::debug("Polling " . $self->{cfg}->{account_nickname});
+
+    my $timeline = eval { $self->{twitter}->home_timeline( { since_id => $self->{last_id} } ) };
+    BarnOwl::debug($@) if $@;
+    unless(defined($timeline) && ref($timeline) eq 'ARRAY') {
+        $self->twitter_error();
+        return;
+    };
+
+    if ($self->{cfg}->{show_mentions}) {
+        my $mentions = eval { $self->{twitter}->mentions( { since_id => $self->{last_id} } ) };
+        BarnOwl::debug($@) if $@;
+        unless (defined($mentions) && ref($mentions) eq 'ARRAY') {
+            $self->twitter_error();
+            return;
+        };
+        #combine, sort by id, and uniq
+        push @$timeline, @$mentions;
+        @$timeline = sort { $b->{id} <=> $a->{id} } @$timeline;
+        my $prev = { id => 0 };
+        @$timeline = grep($_->{id} != $prev->{id} && (($prev) = $_), @$timeline);
+    }
+
+    if ( scalar @$timeline ) {
+        for my $tweet ( reverse @$timeline ) {
+            if ( $tweet->{id} <= $self->{last_id} ) {
+                next;
+            }
+            my $msg = BarnOwl::Message->new(
+                type      => 'Twitter',
+                sender    => $tweet->{user}{screen_name},
+                recipient => $self->{cfg}->{user} || $self->{user},
+                direction => 'in',
+                source    => decode_entities($tweet->{source}),
+                location  => decode_entities($tweet->{user}{location}||""),
+                body      => decode_entities($tweet->{text}),
+                status_id => $tweet->{id},
+                service   => $self->{cfg}->{service},
+                account   => $self->{cfg}->{account_nickname},
+               );
+            BarnOwl::queue_message($msg);
+        }
+        $self->{last_id} = $timeline->[0]{id} if $timeline->[0]{id} > $self->{last_id};
+    } else {
+        # BarnOwl::message("No new tweets...");
+    }
+}
+
+sub poll_direct {
+    my $self = shift;
+
+    return unless BarnOwl::getvar('twitter:poll') eq 'on';
+
+    BarnOwl::debug("Polling direct for " . $self->{cfg}->{account_nickname});
+
+    my $direct = eval { $self->{twitter}->direct_messages( { since_id => $self->{last_direct} } ) };
+    BarnOwl::debug($@) if $@;
+    unless(defined($direct) && ref($direct) eq 'ARRAY') {
+        $self->twitter_error();
+        return;
+    };
+    if ( scalar @$direct ) {
+        for my $tweet ( reverse @$direct ) {
+            if ( $tweet->{id} <= $self->{last_direct} ) {
+                next;
+            }
+            my $msg = BarnOwl::Message->new(
+                type      => 'Twitter',
+                sender    => $tweet->{sender}{screen_name},
+                recipient => $self->{cfg}->{user} || $self->{user},
+                direction => 'in',
+                location  => decode_entities($tweet->{sender}{location}||""),
+                body      => decode_entities($tweet->{text}),
+                private => 'true',
+                service   => $self->{cfg}->{service},
+                account   => $self->{cfg}->{account_nickname},
+               );
+            BarnOwl::queue_message($msg);
+        }
+        $self->{last_direct} = $direct->[0]{id} if $direct->[0]{id} > $self->{last_direct};
+    } else {
+        # BarnOwl::message("No new tweets...");
+    }
+}
+
+sub _stripnl {
+    my $msg = shift;
+
+    # strip non-newline whitespace around newlines
+    $msg =~ s/[^\n\S]*(\n+)[^\n\S]*/$1/sg;
+    # change single newlines to a single space; leave multiple newlines
+    $msg =~ s/([^\n])\n([^\n])/$1 $2/sg;
+    # strip leading and trailing whitespace
+    $msg =~ s/\s+$//s;
+    $msg =~ s/^\s+//s;
+
+    return $msg;
+}
+
+sub twitter {
+    my $self = shift;
+
+    my $msg = _stripnl(shift);
+    my $reply_to = shift;
+
+    if($msg =~ m{\Ad\s+([^\s])+(.*)}sm) {
+        $self->twitter_direct($1, $2);
+    } elsif(defined $self->{twitter}) {
+        if(length($msg) > 140) {
+            die("Twitter: Message over 140 characters long.\n");
+        }
+        $self->twitter_command('update', {
+            status => $msg,
+            defined($reply_to) ? (in_reply_to_status_id => $reply_to) : ()
+           });
+    }
+    $self->poll_twitter if $self->{cfg}->{poll_for_tweets};
+}
+
+sub twitter_direct {
+    my $self = shift;
+
+    my $who = shift;
+    my $msg = _stripnl(shift);
+
+    if(defined $self->{twitter}) {
+        $self->twitter_command('new_direct_message', {
+            user => $who,
+            text => $msg
+           });
+        if(BarnOwl::getvar("displayoutgoing") eq 'on') {
+            my $tweet = BarnOwl::Message->new(
+                type      => 'Twitter',
+                sender    => $self->{cfg}->{user} || $self->{user},
+                recipient => $who, 
+                direction => 'out',
+                body      => $msg,
+                private => 'true',
+                service   => $self->{cfg}->{service},
+               );
+            BarnOwl::queue_message($tweet);
+        }
+    }
+}
+
+sub twitter_atreply {
+    my $self = shift;
+
+    my $to  = shift;
+    my $id  = shift;
+    my $msg = shift;
+    if(defined($id)) {
+        $self->twitter("@".$to." ".$msg, $id);
+    } else {
+        $self->twitter("@".$to." ".$msg);
+    }
+}
+
+sub twitter_retweet {
+    my $self = shift;
+    my $msg = shift;
+
+    if($msg->service ne $self->{cfg}->{service}) {
+        die("Cannot retweet a message from a different service.\n");
+    }
+    $self->twitter_command(retweet => $msg->{status_id});
+    $self->poll_twitter if $self->{cfg}->{poll_for_tweets};
+}
+
+sub twitter_follow {
+    my $self = shift;
+
+    my $who = shift;
+
+    my $user = $self->twitter_command('create_friend', $who);
+    # returns a string on error
+    if (defined $user && !ref $user) {
+        BarnOwl::message($user);
+    } else {
+        BarnOwl::message("Following " . $who);
+    }
+}
+
+sub twitter_unfollow {
+    my $self = shift;
+
+    my $who = shift;
+
+    my $user = $self->twitter_command('destroy_friend', $who);
+    # returns a string on error
+    if (defined $user && !ref $user) {
+        BarnOwl::message($user);
+    } else {
+        BarnOwl::message("No longer following " . $who);
+    }
+}
+
+sub nickname {
+    my $self = shift;
+    return $self->{cfg}->{account_nickname};
+}
+
+1;
