The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

# Copyright (C) 2007-8 Thomas Thurman <tthurman@gnome.org>
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.

use strict;
use warnings;
use Getopt::Long;
use XML::Tiny qw(parsefile);
use App::BLT;

Getopt::Long::Configure ("bundling");
GetOptions(
  'check|c'   => \$check,
  'set|s'     => \$set,
  'help|h'    => \$help,
  'version|v' => \$version,
  'force|F'   => \$force,
  'sync|S'    => \$sync,
  'public|P'  => \$check_public,
  'as|a=s'    => \$username,
 );

if ($version) {
  print 'blt version $VERSION\n';
  exit(2);
}

if ($help) {
  print_masthead();
  print_help();
  exit(1);
}

$timeline = $check_public? 'public': 'friends';

if (-e $rc_filename) {

  # We already have the information
  
  my $document = parsefile($rc_filename)->[0];
  $rc_settings{interval} = $document->{'attrib'}->{'interval'};
  die "Interval not defined in $rc_filename\n"
    unless defined $rc_settings{interval};

  if ($username) {
    for (@{$document->{'content'}}) {
      if ($_->{'attrib'}->{'id'} eq $username) {
        $rc_settings{'user'} = $username;
        $rc_settings{'pass'} = $_->{'attrib'}->{'pass'};
        last;
      }
    }
  } else {
    # take the first one
    my $first = $document->{'content'}->[0]->{'attrib'};
    if (defined $first) {
      $rc_settings{'user'} = $first->{'id'};
      $rc_settings{'pass'} = $first->{'pass'};
    }
  }

  die "User was not found in $rc_filename\n"
    unless defined $rc_settings{'user'} && defined $rc_settings{'pass'};

} elsif ($check && $check_public) {
  # We're doing a public timeline check but without a config file.
  # We can do this, but we need to supply some results.
  $rc_settings{'interval'} = 60;

} else {
  # Okay, just create it.

  print_masthead();
  print "Hi! It looks like you haven't used blt before. To get started,\n";
  print "I'll just need two pieces of information about your twitter\n";
  print "account. If you don't have a twitter account yet, you can get\n";
  print "one at <http://twitter.com/signup>.\n\n";

  print "What is your username on twitter? ";
  my $user = <STDIN>;
  chomp $user;

  print "And what is your password on twitter? ";
  my $pass = <STDIN>;
  chomp $pass;

  # write all that out to an XML file
  # FIXME: Need to check that they don't use crazy entity-needing
  # chars before release
  
  print "Writing out to $rc_filename... ";
  open RC, ">$rc_filename" or die "Can't open file $rc_filename: $!\n";
  print RC "<?xml version=\"1.0\"?>\n<bltrc interval=\"60\">";
  print RC "<ac id=\"$user\" pass=\"$pass\"/></bltrc>\n";
  close RC or die "Can't close file $rc_filename: $!\n";

  %rc_settings = (
    %rc_settings,
    'user' => $user,
    'pass' => $pass,
  );

  # Also attempt to put ourselves into PROMPT_COMMAND in ~/.bashrc
  add_to_bashrc();
}

# TODO: Use file date of empty file rather than reading file
if (-e $last_fetch_filename) {
  open LASTFETCH, "<$last_fetch_filename" or die "Can't open $last_fetch_filename: $!";
  $last_fetch = <LASTFETCH>;
  close LASTFETCH or die "Can't close $last_fetch_filename: $!";

  die "The file $last_fetch_filename has become corrupted; please delete it ".
    "and run $0 again" unless defined $last_fetch;
} else {
  $last_fetch = 0;
}

# ...and we're off!

our $content_filename = "$home/.blt_content";

if ($check) {

  # Too soon to check?

  if (!$force &&
    $last_fetch+$rc_settings{interval} > time) {

    # It's too soon to check. But if the background process
    # isn't running, we should check whether there's content
    # to supply.

    if (-e $content_filename && !already_running_in_background()) {

      open CONTENT, "<$content_filename" or die "Can't open $content_filename: $!";
      my $content = <CONTENT>;
      close CONTENT or die "Can't close $content_filename: $!";

      if ($content) {
        print $content;

        # And now we've printed it, delete the content.
        unlink $content_filename;
      }
    }

    # Anyway, whatever, it's too soon to check, so we should leave.
    exit(0);
  }

  # If we get here, we need to check and it's a good time to do so.

  if ($sync) {
    # Synchronous check; just go and find the
    # data and print it. That's easy.
    print twitter_following($last_fetch);
    exit(0);
  }

  # Otherwise, if we have a copy running in the
  # background, we can bail because someone's
  # dealing with the problem.

  exit(0) if already_running_in_background();

  # Okay, so the problem needs dealing with and
  # nobody's doing anything about it. So I suppose
  # it's down to us.

  exit(0) if fork(); # Go background

  open PIDFILE, ">$pid_filename" or die "$0: can't open $pid_filename: $!";
  print PIDFILE $$;
  close PIDFILE or die "$0: can't close $pid_filename: $!";

  my $content = twitter_following($last_fetch);
  open CONTENT, ">>$content_filename" or die "Can't open $content_filename: $!";
  print CONTENT $content;
  close CONTENT or die "Can't close $content_filename: $!";

  unlink $pid_filename or die "$0: Can't delete $pid_filename: $!";

} elsif (@ARGV) {

  # so it must be --set, the default.
  die "You can't post to the public timeline!" if $check_public;

  my $status = join(' ', @ARGV);
  
  twitter_post($status);

} else {
  print_masthead();
  print_help();
}

=head1 NAME

blt - bash loves twitter - command-line client for twitter

=head1 SYNOPSIS

 blt eating ice-cream and loving python
 blt "It's all in a day's work when you protect apostrophes"
 blt --check --public --sync

=head1 DESCRIPTION

blt is a command-line client for twitter.com, designed to integrate
helpfully with F<bash>(1).  It makes sending messages simple, and
receiving them as simple as with the old F<biff>(1) program which
told you when you had new mail.

=head1 WHAT IS TWITTER?

Twitter is a blogging system which limits posts (known as "tweets")
to 140 characters.  This means that users can receive them easily
over text messaging, instant messaging, and so on.

Twitter allows you to send three kinds of messages:

=over 4

=item Public
The ordinary kind, directed at everyone, to tell them
what you're currently doing or thinking.

=item Directed
which are pointed at one particular user,
but still public.  These contain "@username".  A user can opt
to not be told about directed messages not aimed at people
they know.

=item Private
These are only seen by the sender and the
recipient.  blt can send these using the "d" notation (see
the F<-s> mode below) but does not yet show them when you
ask it to check.

=back 4

=head1 MODES

These are all lowercase letters, and you may have exactly one
of them.  The default mode is F<s>.  You may "bundle" the short
forms of modes and switches together.

=over 4

=item F<-s>, F<--set>

Send a message to Twitter.  The rest of the line is the message
to send.  If this begins with "d" and a space, and then a username
and a space, this will send a private message to that user.  This
is a Twitter convention and not part of the design of blt.

=item F<-c>, F<--check>

Checks whether recent messages have come in for you.

=item F<-a user>, F<--as=user>

Uses the given user's details rather than the default user.
This makes no difference on the public timeline.

=item F<-v>, F<--version>

Prints the current version number and exits.

=item F<-h>, F<--help>

Prints some basic help and exits.

=back 4

=head1 SWITCHES

These are all capitals; you may combine as many as you wish.

=over 4

=item F<-F>, F<--force>

Check even if we checked recently.  Overuse of this option may transgress
Twitter's acceptable use policy.  This is silently ignored if we're
reading the public timeline, because there is no cache requirement there
anyway.

=item F<-S>, F<--sync>

Don't return until all the information has been gathered and printing (see
ASYNCHRONICITY below).

=item F<-P>, F<--public>

Read the public timeline, and not a user's timeline.  This may only be combined
with checking, not setting; an attempt to set on the public timeline causes
an error.

No caching is required.  Nor is any authentication; hence this is the only
instance when F<~/.bltrc.xml> need not exist, and blt will not attempt to create it
if it is not found.

=head1 THE PUBLIC TIMELINE

The public timmeline is a list of all public posts made to twitter
in the last few seconds.  Unlike all other modes, the public
timeline does not require any authentication on your part.

You can read the public timeline with no configuration file.
This will mean that the minimum time between fetching each new
set of information from Twitter is 60 seconds.

=head1 ASYNCHRONICITY

If you are checking a timeline on Twitter, you can run blt
either synchronously (with -S) or asynchronously 
(without -S).  If you run it synchronously, it will go away and fetch the 
information from twitter.com and not return until it's done.

If you run it asynchronously, though, one of three things will happen:

=over 4

=item *

There can be twitter information stored in blt's cache; if so,
it will print this and exit.

=item *

Otherwise, if no blt background process is running, blt will 
start one and exit.  The background process will get the information
from twitter and then exit.

=item *

Otherwise there b<is> a background process running, so blt leaves it 
alone and exits.

=back 4

Asynchronous mode exists because Twitter can be very slow to respond sometimes,
although this has been improving recently.

=head1 HOW BASH CALLS BLT

F<bash>(1) calls the program given in the variable F<PROMPT_COMMAND>
every time you press return.  When you create a F<~/.bltrc.xml>, blt
also attempts to insert a line into F<~/.bashrc> setting this variable
so that blt will be called every time you press return.

=head1 THE CONFIGURATION FILE

The configuration file is a simple piece f XML called F<~/.bltrc.xml>.
Its root node is F<bltrc>, which has one attribute, F<interval>.
This is the minimum time that must pass between fetching information
from Twitter.  (The Twitter acceptable use policy requires this to
be at least 52 seconds.)

Within this root element are a number of F<ac> elements, each
representing an account, with F<id> and F<pass> elements.
On initial creation, only one account
exists.  You may add new accounts in the same format and
select them using the F<-a> switch; see above under SWITCHES.

=head1 BUGS

=over 4

=item *
It can send but not receive private/direct messages.

=item *
blt is pretty fast, but you still might not want to run blt
from ~/.bashrc on a particularly slow computer.

=back 4

=head1 AUTHOR

Thomas Thurman, tthurman at gnome dot org.