package LJ::Simple;

use strict;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

require Exporter;
require AutoLoader;

@ISA = qw(Exporter AutoLoader);
@EXPORT_OK = qw();
@EXPORT = qw();
$VERSION = '0.01';

## Bring in modules we use
use strict;   # Silly not to be strict
use Socket;   # Required for talking to the LJ server
use POSIX;    # For errno values and other POSIX functions

## Helper function prototypes
sub Debug(@);
sub EncVal($$);
sub DecVal($);
sub SendRequest($$$$);
sub dump_list($$);
sub dump_hash($$);

=pod

=head1 NAME

LJ::Simple - A perl module to access LiveJournal via its flat protocol

=head1 SYNOPSIS

C<LJ::Simple> is an object based C<perl> module which is used to access
LiveJournal-based web logs. This module implements most of the
flat protocol LiveJournal uses; for details of this protocol please
see: L<http://www.livejournal.com/developer/protocol.bml>

=head1 REQUIREMENTS

This module requires nothing other than the modules which come with the
standard perl 5.6.1 distribution. The only modules it B<requires> are
C<POSIX> and C<Socket>.

If you have the C<Digest::MD5> module available then the code will make use of
encrypted passwords automatically. However C<Digest::MD5> is not required for
this module to work.

=head1 DESCRIPTION

C<LJ::Simple> is a trival API to access LiveJournal. Currently it
allows you to:

=over 2

=item Login

Log into the LiveJournal system

=item Post

Post a new journal entry in the LiveJournal system

=item Synchronise

Returns a list of journal entries created or modified from a given
date.

=item Edit

Edit the contents of an existing entry within the LiveJournal system

=item Delete

Delete an existing post from the LiveJournal system

=back

=head1 EXAMPLE

The following simple examples shows you how to use the module to post a
simple LiveJournal entry.

=head2 Using LJ::Simple::QuickPost()

C<LJ::Simple::QuickPost()> is a routine which allows you to quickly post an entry into
LiveJournal; as such it lacks a lot of the abilities which using the object-based
interface provides. The C<LJ::Simple::QuickPost()> routine is explained in depth below, however
the following example shows how it can be used to easily post to LiveJournal:

  use LJ::Simple;
  
  LJ::Simple::QuickPost(
  user  =>  "test",
  pass  =>  "test",
  entry =>  "Just a simple entry",
    ) || die "$0: Failed to post entry: $LJ::Simple::error\n";

=head2 Using the standard calls

  use LJ::Simple;

  # Log into the server
  my $lj = new LJ::Simple ({
          user    =>      "test",
          pass    =>      "test",
          site    =>      undef,
        });
  (defined $lj)
    || die "$0: Failed to log into LiveJournal: $LJ::Simple::error\n";
  
  # Prepare the event
  my %Event=();
  $lj->NewEntry(\%Event) ||
    die "$0: Failed to create new entry: $LJ::Simple::error\n";
  
  # Put in the entry
  my $entry=<<EOF;
  A simple entry made using <tt>LJ::Simple</tt> version $LJ::Simple::VERSION
  EOF
  $lj->SetEntry(\%Event,$entry)
    || die "$0: Failed to set entry: $LJ::Simple::error\n";
  
  # Say we are happy
  $lj->SetMood(\%Event,"happy")
    || die "$0: Failed to set mood: $LJ::Simple::error\n";
  
  # Post the event
  my ($item_id,$anum,$html_id)=$lj->PostEntry(\%Event);
  (defined $item_id)
    || die "$0: Failed to post journal entry: $LJ::Simple::error\n";

=head1 VARIABLES

There are various variables which can be used to control certain
aspects of the module. It is generally recommended that if you
wish to change these variables that you do so B<before> you
create the initial object.

The variable you are most likely to use is C<$LJ::Simple::error>
which holds error messages if any of the C<LJ::Simple> calls
fail.

=over 4

=item $LJ::Simple::error

Holds error messages, is set with a blank string at the
start of each method. Whilst the messages are relatively free-form,
there are some prefixes which are sometimes used:

  CODE:     An error in the code calling the API
  INTERNAL: An internal error in this module

=item $LJ::Simple::debug

If set to C<1>, debugging messages are sent to stderr. 

=item $LJ::Simple::protocol

If set to C<1> the protocol used to talk to the remote server is sent to stderr.

=item $LJ::Simple::raw_protocol

If set to C<1> the raw protocol used to talk to the remote server is sent to stderr;
this is only useful if you are doing debugging on C<LJ::Simple> itself as the protocol
is shown as the module gets it from the server; non-printable characters are converted
to their octal presentation form, I<ie> a newline becomes C<\012>.

It should be noted that if C<$LJ::Simple::raw_protocol> is set along with
C<$LJ::Simple::protocol> then the raw protocol display takes precedence for data
returning from the LJ server.

=item $LJ::Simple::UTF

If set to C<1> the LiveJournal server is told to expect UTF-8 encoded characters.
If you enable this the module will attempt to use the utf8 perl module.

The default is see if we have a version of Perl with UTF-8 support and use
it if its available.

=item $LJ::Simple::challenge

If set to C<1> we make use of the challenge-response system instead of using
plain or hashed passwords. This does add some overhead into processing requests
since every action has to be preceeded by a request for a challenge value from
the server.

The default is to see if we have the C<Digest::MD5> module available and if
so we make use of the challenge-response system. This can be disabled by
setting the variable to C<0>.

=item $LJ::Simple::timeout

The time - specified in seconds - to wait for data from the server. If
given a value of C<undef> the API will block until data is avaiable.

=item $LJ::Simple::NonBlock

By default this is set to C<undef>. When given a reference to a sub-routine this
module will call the given sub-routine at various stages of processing the responses
to the LiveJournal server. This is intended for GUI applications which need to process
event queues, update progress bars, I<etc>. When called the sub-routine is passed a
number of variables which maybe useful; the calling method is:

  &{sub}($mode,$status,$action,$bytes_in,$bytes_out,$time,$waiting)

    $mode      - The mode sent to the LJ server
    $status    - The status of the request; ranges from 0 to 1
    $action    - The action performed
    $bytes_in  - The number of bytes read from the remote server
    $bytes_out - The number of bytes written to the remote server
    $time      - The time taken so far in seconds
    $waiting   - Are we waiting for a response from the server ?

It should be noted that if C<$waiting> is set to C<1> then it is B<highly> recommended
that the sub-routine calls C<select()> itself to provide at least some time delay. If
this is not done it is likely that this module will consume far more CPU than necessary.

An example sub-routine follows:

  sub LJStatus {
    my ($mode,$status,$action,$bytes_in,$bytes_out,$time,$waiting) = @_;
    print "\$mode      = $mode\n";
    print "\$status    = $status\n";
    print "\$action    = $action\n";
    print "\$bytes_in  = $bytes_in\n";
    print "\$bytes_out = $bytes_out\n";
    print "\$time      = $time\n";
    print "\$waiting   = $waiting\n";
    print "\n";
    ($waiting) && select(undef,undef,undef,0.5);
  }
  
  $LJ::Simple::NonBlock=\&LJStatus;

=item $LJ::Simple::ProtoSub

By default this points to a sub-routine within the module; this is called when
the protocol between the module and LiveJournal server is to be shown, in other
words when C<$LJ::Simple::protocol> is set to C<1>. The sub-routine called must
take two variables; it is called in the following way:

  &{sub}($direction,$data,$server,$ip_addr)

    $direction - The direction of the flow; 0 means from client to server
                 and 1 means from server to client
    $data      - The data which has flowed; there should not be any newlines
                 with the data, but do not rely on this.
    $server    - The name of the LJ server we are talking to
    $ip_addr   - The IP address of the LJ server we are talking to

If both variables are C<undef> then data is about to flow. If just C<$direction> is
C<undef> then C<$data> holds an informational message.

The standard sub-routine which is called is:

  sub DefaultProtoSub {
    my ($direct,$data,$server,$ip_addr)=@_;
    my $arrow="--> ";
    if (!defined $direct) {
      if (!defined $data) {
        print STDERR "Connecting to $server [$ip_addr]\n";
        print STDERR "Lines starting with \"-->\" is data SENT to the server\n";
        print STDERR "Lines starting with \"<--\" is data RECEIVED from the server\n";
        return;
      }
      $arrow="";
    } else {
      ($direct) && ($arrow="<-- ");
    }
    print STDERR "$arrow$data\n";
  }
  
  $LJ::Simple::ProtoSub=\&DefaultProtoSub;

=item $LJ::Simple::buffer

The number of bytes to try and read in on each C<sysread()> call.

=back

=cut

sub DefaultProtoSub {
  my ($direct,$data,$server,$ip_addr)=@_;
  my $arrow="--> ";
  if (!defined $direct) {
    if (!defined $data) {
      print STDERR "Connecting to $server [$ip_addr]\n";
      print STDERR "Lines starting with \"-->\" is data SENT to the server\n";
      print STDERR "Lines starting with \"<--\" is data RECEIVED from the server\n";
      return;
    }
    $arrow="";
  } else {
    ($direct) && ($arrow="<-- ");
  }
  print STDERR "$arrow$data\n";
}


## Global variables - documented
# Debug ?
$LJ::Simple::debug=0;
# Show protocol ?
$LJ::Simple::protocol=0;
# Protocol handling code
$LJ::Simple::ProtoSub=\&DefaultProtoSub;
# Show raw protocol ?
$LJ::Simple::raw_protocol=0;
# Use UTF-8 ?
$LJ::Simple::UTF = undef;
# Use challenge-response ?
$LJ::Simple::challenge = undef;
# Use non-block sub-routine
$LJ::Simple::NonBlock = undef;
# Errors
$LJ::Simple::error="";
# Timeout for reading from sockets - default is 5 minutes
$LJ::Simple::timeout = 300;
# How much data to read from the socket in one read()
$LJ::Simple::buffer = 8192;

# How much times to retry if some network error occurs
$LJ::Simple::network_retries = 10;
# How much seconds to wait before each retry
$LJ::Simple::network_sleep = 2;
# LJ Client string
$LJ::Simple::LJ_Client = "LJ::Simple/$VERSION";
# UserAgent string
$LJ::Simple::UserAgent = "LJ::Simple/$VERSION; http://www.bpfh.net/computing/software/LJ::Simple/; lj-simple\@bpfh.net";

## Global variables - internal and undocumented
# Should we not fully run the QuickPost routine ?
$LJ::Simple::TestStopQuickPost = 0;

## Internal variables - private to this module
# Standard ports
my %StdPort = (
  http    =>  80,
  http_proxy  =>  3128,
);

=pod

=head1 AVAILABLE METHODS

=head2 LJ::Simple::QuickPost()

C<LJ::Simple::QuickPost()> is a routine which allows you to quick post to LiveJournal.
However it does this by hiding a lot of the details involved in using
C<LJ::Simple> to do this. This routine will do all of the work involved in
logging into the LiveJournal server, preparing the entry and then posting it.
If at any stage there is a failure then C<0> is returned and C<$LJ::Simple::error>
will contain the reason why. If the entry was successfully posted to the LiveJournal
server then the routine will return C<1>.

There are a number of options to the C<LJ::Simple::QuickPost()> routine:

  LJ::Simple::QuickPost(
  user  =>  Username
  pass  =>  Password
  entry =>  Contents of the entry
  subject =>  Subject line of the entry
  mood  =>  Current mood
  music =>  Current music
  html  =>  HTML content ?
  protect =>  Security settings of the entry
  groups  =>  Friends groups list
  );

Of these, only the C<user>, C<pass> and C<entry> options are required; all of the other
options are optional. The option names are all case insensitive.

=over 4

=item user

The username who owns the journal the entry should be posted to;
this option is B<required>.

=item pass

The password of the C<user>;
this option is B<required>.

=item entry

The actual entry itself;
this option is B<required>.

=item subject

The subject line of the post.

=item mood

The mood to associate with the post; the value is given to the C<SetMood()> method
for processing.

=item music

The music to associate with the post.

=item html

This is a boolean value of either C<1> or C<0>. If you want to say that the entry
contains HTML and thus should be considered to be preformatted then set C<html> to
C<1>. Otherwise you can either set it to C<0> or not give the option.

=item protect

By default the new entry will be public unless you give the C<protect> option. This
option should be given the protection level required for the post and can be one of
the following:

  public  - The entry is public
  friends - Entry is friends-only
  groups  - Entry is restricted to friends groups
  private - Entry is restricted to the journal's owner

If you set the C<protect> option to C<groups> you must also include the C<groups>
option - see below for details.

=item groups

If the C<protect> option is set to C<groups> then this option should contain a
list reference which contains the list of groups the entry should be restricted to.
This option is B<required> if the C<protect> option is set to C<groups>.

=back

Example code:

  # Simple test post
  LJ::Simple::QuickPost(
  user  =>  "test",
  pass  =>  "test",
  entry =>  "Just a simple entry",
    ) || die "$0: Failed to post entry: $LJ::Simple::error\n";
  
  # A friends-only preformatted entry
  LJ::Simple::QuickPost(
  user  =>  "test",
  pass  =>  "test",
  entry =>  "<p>Friends-only, preformatted, entry</p>",
  html  =>  1,
  protect =>  "friends",
    ) || die "$0: Failed to post entry: $LJ::Simple::error\n";
  
  # A entry restricted to several friends groups
  LJ::Simple::QuickPost(
  user  =>  "test",
  pass  =>  "test",
  entry =>  "Entry limited to friends groups",
  protect =>  "groups",
  groups  =>  [qw( one_group another_group )],
    ) || die "$0: Failed to post entry: $LJ::Simple::error\n";

=cut
sub QuickPost(@) {
  my %opts=();
  my @prot_opts=();
  while($#_>-1) {
    my $k=lc(shift(@_));
    my $v=shift(@_);
    (defined $v) || next;
    $opts{$k}=$v;
  }
  foreach (qw( user pass entry )) {
    (exists $opts{$_}) && next;
    $LJ::Simple::error="CODE: QuickPost() called without the required $_ option";
    return 0;
  }
  if ((exists $opts{html}) && ($opts{html}!~/^[01]$/)) {
    $LJ::Simple::error="CODE: QuickPost() not given either 0 or 1 for html option";
    return 0;
  }
  if ((exists $opts{protect}) && ($opts{protect} eq "groups")) {
    if (!exists $opts{groups}) {
      $LJ::Simple::error="CODE: QuickPost() given protect=groups, but no groups option";
      return 0;
    }
    if (ref($opts{groups}) ne "ARRAY") {
      $LJ::Simple::error="CODE: QuickPost() not given a list reference for the groups option";
      return 0;
    }
    @prot_opts=@{$opts{groups}};
  }

  # Kludge so we can test the input validation
  ($LJ::Simple::TestStopQuickPost) && return 1;
  
  my $lj = new LJ::Simple({
  user  =>  $opts{user},
  pass  =>  $opts{pass},
  });
  (defined $lj) || return 0;

  my %Event=();
  $lj->NewEntry(\%Event) || return 0;
  $lj->SetEntry(\%Event,$opts{entry}) || return 0;
  (exists $opts{subject}) &&
    ($lj->SetSubject(\%Event,$opts{subject}) || return 0);
  (exists $opts{mood}) &&
    ($lj->SetMood(\%Event,$opts{mood}) || return 0);
  (exists $opts{music}) &&
    ($lj->Setprop_current_music(\%Event,$opts{music}) || return 0);
  (exists $opts{html}) &&
    ($lj->Setprop_preformatted(\%Event,$opts{html}) || return 0);
  (exists $opts{protect}) &&
    ($lj->SetProtect(\%Event,$opts{protect},@prot_opts) || return 0);

  my ($item_id,$anum,$html_id)=$lj->PostEntry(\%Event);
  (defined $item_id) || return 0;

  return 1;
}

=pod

=head2 Object creation

=over 4

=item login

Logs into the LiveJournal system.

  ## Simplest logon method
  my $lj = new LJ::Simple ( {
    user  =>  "username",
    pass  =>  "password",
    } );
  
  ## Login with options
  my $lj = new LJ::Simple ( {
    user  =>  "username",
    pass  =>  "password",
    site  =>  "hostname[:port]",
    proxy =>  "hostname[:port]",
    moods =>  0 | 1,
    pics  =>  0 | 1,
    fast  =>  0 | 1,
    } );

  ## Login by using login()
  my $lj = LJ::Simple->login ( {
    user  =>  "username",
    pass  =>  "password",
    site  =>  "hostname[:port]",
    proxy =>  "hostname[:port]",
    moods =>  0 | 1,
    pics  =>  0 | 1,
    fast  =>  0 | 1,
    } );

Where:

  user     is the username to use
  pass     is the password associated with the username
  site     is the remote site to use
  proxy    is the HTTP proxy site to use; see below.
  moods    is set to 0 if we do not want to download the mood
           list. Defaults to 1
  pics     is set to 0 if we do not want to download the user
           picture information. Defaults to 1
  fast     is set to 1 if we want to perform a fast login.
           Default is 0. See below for details of this.

Sites defined in C<site> or C<proxy> are a hostname with an
optional port number, separated by a C<:>, i.e.:

  www.livejournal.com
  www.livejournal.com:80

If C<site> is given C<undef> then the code assumes that you wish to
connect to C<www.livejournal.com:80>. If no port is given then port
C<80> is the default.

If C<proxy> is given C<undef> then the code will go directly to the
C<$site> unless a suitable environment variable is set.
If no port is given then port C<3128> is the default.

C<LJ::Simple> also supports the use the environment variables C<http_proxy>
and C<HTTP_PROXY> to store the HTTP proxy server details. The format of these
environment variables is assumed to be:

  http://server[:port]/

Where C<server> is the name of the proxy server and the optional C<port> the
proxy server is on - port C<3128> is used if no port is explicitly given.

It should be noted that the proxy environment variables are B<only> checked
if the C<proxy> value is B<NOT> given to the C<LJ::Simple> object creation.
Thus to disable looking at the proxy environment variables use
C<proxy=E<gt>undef> in C<new()> or C<login()>.

If C<moods> is set to C<0> then the mood list will not be pulled from
the LiveJournal server and the following functions will be affected:

  o moods() will always return undef (error)
  o Setprop_current_mood_id() will not validate the mood_id
    given to it.
  o SetMood() will not attempt to convert the string it is
    given into a given mood_id

If C<pics> is set to C<0> then the data on the user pictures will
not be pulled from the LiveJournal server and the following
functions will be affected:

  o pictures() will always return undef (error)
  o Setprop_picture_keyword() will blindly set the picture keyword
    you give it - no validation will be performed.
  o DefaultPicURL() will always return undef (error)

If C<fast> is set to C<1> then we will perform a I<fast login>. Essentially
all this does is to set up the various entries in the object hash which
the routines called after C<login> expect to see; at no time does it talk to
the LiveJournal servers. What this means is that it is very fast. However it
also means that when you use parts of the API which B<do> talk to the LiveJournal
servers its quite possible that you will get back errors associated with
authentication errors, network outages, I<etc>. In other words, in C<fast> mode
the login will always succeed, no matter what the state the LiveJournal
server we're talking is in. It should be noted that the following functions
will be affected if you enable the I<fast login>:

  o moods() will always return undef (error)
  o Setprop_current_mood_id() will not validate the mood_id
    given to it
  o SetMood() will not attempt to convert the string it is
    given into a given mood_id
  o pictures() will always return undef (error)
  o Setprop_picture_keyword() will blindly set the picture keyword
    you give it - no validation will be performed
  o communities() will always return an empty list
  o MemberOf() will always return 0 (error)
  o UseJournal() will not validate the shared journal name you
    give it
  o groups() will always return undef (error)
  o MapGroupToId() will always undef (error)
  o MapIdToGroup() will always undef (error)
  o SetProtectGroups() will always 0 (error)
  o message() will always return undef (error)
  o The key of "groups" in the list of hashes returned by
    GetFriends() will always point to an empty list
  o CheckFriends() will return undef (error) if you give it a
    list of groups

On success this sub-routine returns an C<LJ::Simple> object. On
failure it returns C<undef> with the reason for the failure being
placed in C<$LJ::Simple::error>.

Example code:

  ## Simple example, going direct to www.livejournal.com:80
  my $lj = new LJ::Simple ({ user => "someuser", pass => "somepass" });
  (defined $lj) ||
    die "$0: Failed to access LiveJournal - $LJ::Simple::error\n";

  ## More complex example, going via a proxy server on port 3000 to a
  ## a LiveJournal system available on port 8080 on the machine
  ## www.somesite.com.
  my $lj = new LJ::Simple ({ 
  user  => "someuser",
  pass  => "somepass", 
  site  => "www.somesite.com:8080",
  proxy => "proxy.internal:3000",
  });
  (defined $lj) ||
    die "$0: Failed to access LiveJournal - $LJ::Simple::error\n";

  ## Another complex example, this time saying that we do not want
  ## the mood list or user pictures downloaded
  my $lj = new LJ::Simple ({ 
  user  => "someuser",
  pass  => "somepass", 
  pics  => 0,
  moods => 0,
  });
  (defined $lj) ||
    die "$0: Failed to access LiveJournal - $LJ::Simple::error\n";
  
  ## Final example - this one shows the use of the fast logon
  my $lj = new LJ::Simple ({ 
  user  => "someuser",
  pass  => "somepass", 
  fast  => 1,
  });
  (defined $lj) ||
    die "$0: Failed to access LiveJournal - $LJ::Simple::error\n";

=cut
##
## Log into the LiveJournal system. Given that the LJ stuff is just
## layered over HTTP, its not essential to do this. However it does
## mean that we can check the auth details, get some useful info for
## later, etc.
##
sub login($$) {
  # Handle the OOP stuff
  my $this=shift;
  $LJ::Simple::error="";
  if ($#_ != 0) {
    $LJ::Simple::error="CODE: Incorrect usage of login() for argv - see docs";
    return undef;
  }
  # Get the hash
  my $hr = shift;
  my $class = ref($this) || $this;
  my $self = {};
  bless $self,$class;
  if ((!exists $hr->{user})||($hr->{user} eq "") ||
      (!exists $hr->{pass})||($hr->{pass} eq "")) {
    $LJ::Simple::error="CODE: Incorrect usage of login() - see docs";
    return undef;
  }
  $self->{auth}={
  user    =>  $hr->{user},
  pass    =>  $hr->{pass},
  challenge =>  {},
  };
  if (! defined $LJ::Simple::UTF) {
    eval { require utf8 };
    if (!$@) {
      $LJ::Simple::UTF=1;
      Debug("UTF-8 support found");
    } else {
      $LJ::Simple::UTF=0;
      Debug("No UTF-8 support found");
    }
  } elsif ($LJ::Simple::UTF) {
    eval { require utf8 };
    if (!$@) {
      Debug("Using UTF-8 as requested");
    } else {
      $LJ::Simple::error="CODE: no UTF-8 support in your version of perl";
      return undef;
    }
  }
  eval { require Digest::MD5 };
  if (!$@) {
    Debug("Using Digest::MD5");
    my $md5=Digest::MD5->new;
    $md5->add($hr->{pass});
    $self->{auth}->{hash}=$md5->hexdigest;
    delete $self->{auth}->{pass};
    (!defined $LJ::Simple::challenge) && ($LJ::Simple::challenge=1);
  } else {
    if ((defined $LJ::Simple::challenge)&&($LJ::Simple::challenge)) {
      $LJ::Simple::error="Challenge-response auth requested, no Digest::MD5 found";
      return undef;
    }
    $LJ::Simple::challenge=0;
  }
  if ((exists $hr->{site})&&(defined $hr->{site})&&($hr->{site} ne "")) {
    my $site_port=$StdPort{http};
    if ($hr->{site}=~/\s*(.*?):([0-9]+)\s*$/) {
      $hr->{site} = $1;
      $site_port = $2;
    }
    $self->{lj}={
    host  =>  $hr->{site},
    port  =>  $site_port,
    }
  } else {
    $self->{lj}={
    host  =>  "www.livejournal.com",
    port  =>  $StdPort{http},
    }
  }
  if ((exists $hr->{proxy})&&(defined $hr->{proxy})&&($hr->{proxy} ne "")) {
    my $proxy_port=$StdPort{http_proxy};
    if ($hr->{proxy}=~/\s*(.*?):([0-9]+)\s*$/) {
      $hr->{proxy} = $1;
      $proxy_port = $2;
    }
    $self->{proxy}={
    host  =>  $hr->{proxy},
    port  =>  $proxy_port,
    };
  } elsif (!exists $hr->{proxy}) {
    # Getting proxy details from the environment; assumes that the proxy is
    # given as http://site[:port]/
    # The first matching env is used.
    foreach my $env (qw( http_proxy HTTP_PROXY )) {
      (exists $ENV{$env}) || next;
      ($ENV{$env}=~/^(?:http:\/\/)([^:\/]+)(?::([0-9]+)){0,1}/o) || next;
      $self->{proxy}={
    host  =>  $1,
    port  =>  $2,
      };
      (defined $self->{proxy}->{port}) || ($self->{proxy}->{port}=$StdPort{http_proxy});
    }
  } else {
    $self->{proxy}=undef;
  }

  # Set fastserver to 0 until we know better
  $self->{fastserver}=0;

  if ((exists $hr->{fast}) && ($hr->{fast}==1)) {
    ## Doing fast login, so return object
    Debug(dump_hash($self,""));
    return $self;
  }
  
  my $GetMoods=1;
  if ((exists $hr->{moods}) && ($hr->{moods}==0)) {
    $GetMoods=0;
  }
  my $GetPics=1;
  if ((exists $hr->{pics}) && ($hr->{pics}==0)) {
    $GetPics=0;
  }

  # Perform the actual login
  $self->SendRequest("login", {
  "moods"   =>  $GetMoods,
  "getpickws" =>  $GetPics,
  "getpickurls" =>  $GetPics,
    },undef) || return undef;

  # Now see if we can set fastserver
  if ( (exists $self->{request}->{lj}->{fastserver}) &&
       ($self->{request}->{lj}->{fastserver} == 1) ) {
    $self->{fastserver}=1;
  }

  # Moods
  $self->{moods}=undef;
  $self->{mood_map}=undef;
  # Shared access journals
  $self->{access}=undef;
  # User groups
  $self->{groups}=undef;
  # Images defined
  $self->{pictures}=undef;
  # Default URL
  $self->{defaultpicurl}=undef;
  # Message from LJ
  $self->{message}=undef;

  # Handle moods, etc.
  my ($k,$v)=(undef,undef);
  while(($k,$v) = each %{$self->{request}->{lj}}) {

    # Message from LJ
    if ($k eq "message") {
      $self->{message}=$v;

    # Moods
    } elsif ($k=~/^mood_([0-9]+)_([a-z]+)/o) {
      my ($id,$type)=($1,$2);
      if (!defined $self->{moods}) {
        $self->{moods}={};
      }
      if (!exists $self->{moods}->{$id}) {
        $self->{moods}->{$id}={};
      }
      if ($type eq "id") {
        $self->{moods}->{$id}->{id}=$v;
      } elsif ($type eq "name") {
        $self->{moods}->{$id}->{name}=$v
      }

    # Picture key words
    } elsif ($k=~/^(pickw_count)/o) {
      if (!defined $self->{pictures}) {
        $self->{pictures}={};
      }
    } elsif ($k eq "defaultpicurl") {
      $self->{defaultpicurl}=$v;
    } elsif ($k=~/^(pickw[^_]*)_([0-9]+)/o) {
      my ($type,$id)=($1,$2);
      if (!defined $self->{pictures}) {
        $self->{pictures}={};
      }
      if (!exists $self->{pictures}->{$id}) {
        $self->{pictures}->{$id}={};
      }
      if ($type eq "pickwurl") {
        $self->{pictures}->{$id}->{url}=$v;
      } elsif ($type eq "pickw") {
        $self->{pictures}->{$id}->{name}=$v
      }

    # Shared access journals
    } elsif ($k=~/^access_([0-9]+)/) {
      if (!defined $self->{access}) {
        $self->{access}={};
      }
      $self->{access}->{$v}=1;

    # Groups
    } elsif ($k=~/^frgrp_([0-9]+)_(.*)/) {
      my ($id,$type)=($1,$2);
      if (!defined $self->{groups}) {
        $self->{groups}={
          src  => {},  # Source data
          id   => {},  # Id -> name mapping
          name => {},  # Real data, name keyed
        };
      }
      if (!exists $self->{groups}->{src}->{$id}) {
        $self->{groups}->{src}->{$id}={};
      }
      if ($type eq "sortorder") {
        $self->{groups}->{src}->{$id}->{sort}=$v;
      } elsif ($type eq "name") {
        $self->{groups}->{src}->{$id}->{name}=$v
      }
    }
  }

  ## We now handle the group hash fully. Note in the case
  ## of groups having the same name, only the first will
  ## go into the name hash.
  ($k,$v)=(undef,undef);
  while(($k,$v)=each %{$self->{groups}->{src}}) {
    $self->{groups}->{id}->{$k}=$v->{name};
    if (!exists $self->{groups}->{name}->{$v->{name}}) {
      $self->{groups}->{name}->{$v->{name}} = {
        id   => $k,
        name => $v->{name},
        sort => $v->{sort},
      };
    }
  }

  ##
  ## And now we handle the mood map fully
  ##
  if ($GetMoods) {
    $self->{mood_map}={};
    foreach (values %{$self->{moods}}) {
      $self->{mood_map}->{lc($_->{name})}=$_->{id};
    }
  }

  Debug(dump_hash($self,""));
  
  ## Logged in, so return self.
  return $self;
}

## Define reference from new to login
*new="";
*new=\&login;


=pod

=back

=head2 Getting data from the LiveJournal login

=over 4

=item $lj->message()

Returns back a message set in the LiveJournal system. Either
returns back the message or C<undef> if no message is set.

Example code:

  my $msg = $lj->message();
  (defined $msg) &&
    print "LJ Message: $msg\n";

=cut
sub message($) {
  my $self=shift;
  return $self->{message};
}

=pod

=item $lj->moods($hash_ref)

Takes a reference to a hash and fills it with information about
the moods returned back by the server. Either returns back the
same hash reference or C<undef> on error.

Note that if the LiveJournal
object was created with either C<moods> set to C<0> or
with C<fast> set to C<1> then this function will always return
an error.

The hash the given reference is pointed to is emptied before
it is used and after a successful call the hash given will
contain:

  %hash = (
    list    => [ list of mood names, alphabetical ]
    moods   => {
      mood_name => mood_id
    }
    idents  => {
      mood_id   => mood_name
    }
  )


Example code:

  my %Moods=();
  if (!defined $lj->moods(\%Moods)) {
    die "$0: LJ error - $LJ::Simple::error";
  }
  foreach (@{$Moods{list}}) {
    print "$_ -> $Moods{moods}->{$_}\n";
  }
  

=cut
sub moods($$) {
  my $self=shift;
  my ($hr) = @_;
  $LJ::Simple::error="";
  if (ref($hr) ne "HASH") {
    $LJ::Simple::error="CODE: moods() not given a hash reference";
    return undef;
  }
  if (!defined $self->{moods}) {
    $LJ::Simple::error="Unable to return moods - not requested at login";
    return undef;
  }
  %{$hr}=(
    list  => [],
    moods => {},
    idents  => {},
  );
  my ($k,$v);
  while(($k,$v)=each %{$self->{moods}}) {
    push(@{$hr->{list}},$v->{name});
    $hr->{moods}->{$v->{name}}=$v->{id};
    $hr->{idents}->{$v->{id}}=$v->{name};
  }
  $hr->{list} = [ (sort { $a cmp $b } @{$hr->{list}}) ];
  return $hr;
}

=pod

=item $lj->communities()

Returns a list of shared access communities the user logged in can
post to. Returns an empty list if no communities are available

Example code:

  my @communities = $lj->communities();
  print join("\n",@communities),"\n";

=cut
sub communities($) {
  my $self=shift;
  $LJ::Simple::error="";
  (defined $self->{access}) || return ();
  return sort {$a cmp $b} (keys %{$self->{access}});
}


=pod

=item $lj->MemberOf($community)

Returns C<1> if the user is a member of the named community. Returns
C<0> otherwise.

Example code:

  if ($lj->MemberOf("some_community")) {
     :
     :
     :
  }

=cut
sub MemberOf($$) {
  my $self=shift;
  my ($community)=@_;
  $LJ::Simple::error="";
  (defined $self->{access}) || return 0;
  return (exists $self->{access}->{$community});
}

=pod

=item $lj->groups($hash_ref)

Takes a reference to a hash and fills it with information about
the friends groups the user has configured for themselves. Either
returns back the hash reference or C<undef> on error.

The hash the given reference points to is emptied before it is
used and after a successful call the hash given will contain
the following:

   %hash = (
     "name" => {
       "Group name" => {
         id   => "Number of the group",
         sort => "Sort order",
         name => "Group name (copy of key)",
       },
     },
     "id"   => {
       "Id"   => "Group name",
     },
   );

Example code:

  my %Groups=();
  if (!defined $lj->groups(\%Groups)) {
    die "$0: LJ error - $LJ::Simple::error";
  }
  my ($id,$name)=(undef,undef);
  while(($id,$name)=each %{$Groups{id}}) {
    my $srt=$Groups{name}->{$name}->{sort};
    print "$id\t=> $name [$srt]\n";
  }

=cut
sub groups($$) {
  my $self=shift;
  my ($hr) = @_;
  $LJ::Simple::error="";
  if (ref($hr) ne "HASH") {
    $LJ::Simple::error="CODE: groups() not given a hash reference";
    return undef;
  }
  if (!defined $self->{groups}) {
    $LJ::Simple::error="Unable to return groups - none defined";
    return undef;
  }
  %{$hr}=(
    name => {},
    id   => {},
  );
  my ($k,$v);
  while(($k,$v)=each %{$self->{groups}->{id}}) {
    $hr->{id}->{$k}=$v;
  }
  while(($k,$v)=each %{$self->{groups}->{name}}) {
    $hr->{name}->{$k}={};
    my ($lk,$lv);
    while(($lk,$lv)=each %{$self->{groups}->{name}->{$k}}) {
       $hr->{name}->{$k}->{$lk}=$lv;
    }
  }
  return $hr;
}


=pod

=item $lj->MapGroupToId($group_name)

Used to map a given group name to its identity. On
success returns the identity for the group name. On
failure it returns C<undef> and sets
C<$LJ::Simple::error>.

=cut
sub MapGroupToId($$) {
  my $self=shift;
  my ($grp)=@_;
  $LJ::Simple::error="";
  if (!defined $self->{groups}) {
    $LJ::Simple::error="Unable to map group to id - none defined";
    return undef;
  }
  if (!exists $self->{groups}->{name}->{$grp}) {
    $LJ::Simple::error="No such group";
    return undef;
  }
  return $self->{groups}->{name}->{$grp}->{id};
}


=pod

=item $lj->MapIdToGroup($id)

Used to map a given identity to its group name. On
success returns the group name for the identity. On
failure it returns C<undef> and sets
C<$LJ::Simple::error>.

=cut
sub MapIdToGroup($$) {
  my $self=shift;
  my ($id)=@_;
  $LJ::Simple::error="";
  if (!defined $self->{groups}) {
    $LJ::Simple::error="Unable to map group to id - none defined";
    return undef;
  }
  if (!exists $self->{groups}->{id}->{$id}) {
    $LJ::Simple::error="No such group ident";
    return undef;
  }
  return $self->{groups}->{id}->{$id};
}

=pod


=item $lj->pictures($hash_ref)

Takes a reference to a hash and fills it with information about
the pictures the user has configured for themselves. Either
returns back the hash reference or C<undef> on error. Note that
the user has to have defined picture keywords for this to work.

Note that if the LiveJournal
object was created with either C<pics> set to C<0> or
with C<fast> set to C<1> then this function will always return
an error.

The hash the given reference points to is emptied before it is
used and after a successful call the hash given will contain
the following:

   %hash = (
     "keywords" => "URL of picture",
   );

Example code:

  my %pictures=();
  if (!defined $lj->pictures(\%pictures)) {
    die "$0: LJ error - $LJ::Simple::error";
  }
  my ($keywords,$url)=(undef,undef);
  while(($keywords,$url)=each %pictures) {
    print "\"$keywords\"\t=> $url\n";
  }


=cut
sub pictures($$) {
  my $self=shift;
  my ($hr)=@_;
  $LJ::Simple::error="";
  if (!defined $self->{pictures}) {
    $LJ::Simple::error="Unable to return pictures - none defined";
    return undef;
  }
  if (ref($hr) ne "HASH") {
    $LJ::Simple::error="CODE: pictures() not given a hash reference";
    return undef;
  }
  %{$hr}=();
  foreach (values %{$self->{pictures}}) {
    $hr->{$_->{name}}=$_->{url};
  }
  return $hr;
}

=pod

=item $lj->DefaultPicURL()

Returns the URL of the default picture used by the user.

Note that if the LiveJournal
object was created with either C<pics> set to C<0> or
with C<fast> set to C<1> then this function will always return
an error.

Example code:

  print $lj->DefaultPicURL(),"\n";

=cut
sub DefaultPicURL($) {
  my $self=shift;
  $LJ::Simple::error="";
  if (!defined $self->{defaultpicurl}) {
    $LJ::Simple::error="Unable to return default picture URL - none defined";
    return undef;
  }
  return $self->{defaultpicurl};
}

=pod

=item $lj->user()

Returns the username used to log into LiveJournal

Example code:
 
  my $user = $lj->user();

=cut
sub user($) {
  my $self=shift;
  $LJ::Simple::error="";
  return $self->{auth}->{user};
}


=pod

=item $lj->fastserver()

Used to tell if the user which was logged into the LiveJournal system can use the
fast servers or not. Returns C<1> if the user can use the fast servers, C<0>
otherwise.

Example code:

  if ($lj->fastserver()) {
    print STDERR "Using fast server for ",$lj->user(),"\n";
  }

=cut
sub fastserver($) {
  my $self=shift;
  $LJ::Simple::error="";
  return $self->{fastserver};
}

=pod

=back

=head2 Dealing with friends

=over 4

=item $lj->GetFriendOf()

Returns a list of the other LiveJournal users who list the current
user as a friend. The list returned contains a least one entry, the
number of entries in the list. This value can range from 0 to however
many users are in the list. In the event of a failure this value is
undefined.

The list of friends is a list of hash references which contain data
about the users who list the current user as a friend. Each hash
referenced will contain the following:

  {
    user     => The LiveJournal username
    name     => The full name of the user
    fg       => The foreground colour which represents the user
    bg       => The background colour which represents the user
    status   => The status of the user
    type     => The type of the user
  }

Both the C<bg> and C<fg> values are stored in the format of "C<#>I<RR>I<GG>I<BB>"
where the I<RR>, I<GG>, I<BB> values are given as two digit hexadecimal numbers which
range from C<00> to C<ff>.

The C<status> of a user can be one of C<active>, C<deleted>, C<suspended> or C<purged>.

The C<type> of a user can either be C<user> which means that the user is a normal
LiveJournal user or it can be C<community> which means that the user is actually a
community which the current LJ user is a member of.

It should be noted that any of the values in the hash above can be undefined if
that value was not returned from the LiveJournal server.

The returned list is ordered by the LiveJournal login names of the users.

Example code:

  my ($num_friends_of,@FriendOf)=$lj->GetFriendOf();
  (defined $num_friends_of) ||
    die "$0: Failed to get friends of user - $LJ::Simple::error\n";
  print "LJ login\tReal name\tfg\tbg\tStatus\tType\n";
  foreach (@FriendOf) {
    print "$_->{user}\t",
          "$_->{name}\t",
          "$_->{fg}\t",
          "$_->{bg}\t",
          "$_->{status}\t",
          "$_->{type}\n";
  }

=cut
sub GetFriendOf($) {
  my $self=shift;
  $LJ::Simple::error="";
  my %Event=();
  my %Resp=();
  $self->SendRequest("friendof",\%Event,\%Resp) || return undef;
  my %Friends=();
  my ($k,$v);
  while(($k,$v)=each %Resp) {
    ($k=~/^friendof_([0-9]+)_(.*)/) || next;
    my ($id,$type)=($1,$2);
    if (!exists $Friends{$id}) {
      $Friends{$id}={
  user  =>  undef,
  name  =>  undef,
  bg  =>  undef,
  fg  =>  undef,
  status  =>  "active",
  type  =>  "user",
      };
    }
    $Friends{$id}->{$type}=$v;
  }
  my @lst=sort {$a->{user} cmp $b->{user}} (values %Friends);
  return ($#lst+1,@lst);
}


=pod

=item $lj->GetFriends()

Returns a list of the other LiveJournal user who are listed as friends of
the current user. The list returned contains a least one entry, the
number of entries in the list. This value can range from 0 to however
many users are in the list. In the event of a failure this value is
undefined.

The list of friends is a list of hash references which contain data
about the users who list the current user as a friend. Each hash
referenced will contain the following:

  {
    user      => The LiveJournal username
    name      => The full name of the user
    fg        => The foreground colour which represents the user
    bg        => The background colour which represents the user
    dob       => The date of birth for the user
    birthday  => The birthday of the user
    groups    => The list of friends groups this user is in
    groupmask => The actual group mask for this user
    status    => The status of the user
    type      => The type of the user
  }

Both the C<bg> and C<fg> values are stored in the format of "C<#>I<RR>I<GG>I<BB>"
where the I<RR>, I<GG>, I<BB> values are given as two digit hexadecimal numbers which
range from C<00> to C<ff>.

The C<dob> value is stored as a Unix timestamp; that is seconds since epoch. If the
user has no date of birth defined B<or> they have only given their birthday then this
value will be C<undef>.

The C<birthday> value is the date of the user's next birthday given as a Unix timestamp.

The C<groups> value is a reference to a list of the friends group this user is a member
of. It should be noted that to have any items in the list the user must be a
member of a friends group and the C<login()> method must B<not> have been called
with the fast login option.

The C<groupmask> value is the actual group mask for the user. This is used to build
the C<groups> list. It is a 32-bit number where each bit represents membership of a
given friends group. Bits 0 and 31 are reserved; all other bits can be used. The bit
a group corresponds to is taken by bit-shifting 1 by the group id number.

The C<status> of a user can be one of C<active>, C<deleted>, C<suspended> or C<purged>.

The C<type> of a user can either be C<user> which means that the user is a normal
LiveJournal user or it can be C<community> which means that the user is actually a
community which the current LJ user is a member of.

It should be noted that any of the values in the hash above can be undefined if
that value was not returned from the LiveJournal server.

The returned list is ordered by the LiveJournal login names of the users.

Example code:

  use POSIX;
  
  my ($num_friends,@Friends)=$lj->GetFriends();
  (defined $num_friends) ||
    die "$0: Failed to get friends - $LJ::Simple::error\n";
  
  my $f=undef;
  foreach $f (@Friends) {
    foreach (qw(dob birthday)) {
      (defined $f->{$_}) || next;
      $f->{$_}=strftime("%Y/%m/%d",gmtime($f->{$_}));
    }
    my ($k,$v)=(undef,undef);
    while(($k,$v)=each %{$f}) {
      (!defined $v) && ($f->{$k}="[undefined]");
    }
    print "$f->{user}\n";
    print "  Name         : $f->{name}\n";
    print "  Colors       : fg->$f->{fg} bg->$f->{bg}\n";
    print "  DOB          : $f->{dob}\n";
    print "  Next birthday: $f->{birthday}\n";
    print "  Status       : $f->{status}\n";
    print "  Type         : $f->{type}\n";
    if ($#{$f->{groups}}>-1) {
      print "  Friend groups:\n";
      print "    + ",join("\n    + ",@{$f->{groups}}),"\n";
    } else {
      print "  Friend groups: [none]\n";
    }
    print "\n";
  }

=cut
sub GetFriends($) {
  my $self=shift;
  $LJ::Simple::error="";
  my %Event=(
    includegroups =>  1,
    includebdays  =>  1,
  );
  my %Resp=();
  $self->SendRequest("getfriends",\%Event,\%Resp) || return undef;
  my %Friends=();
  my ($k,$v);
  while(($k,$v)=each %Resp) {
    ($k=~/^friend_([0-9]+)_(.*)/) || next;
    my ($id,$type)=($1,$2);
    if (!exists $Friends{$id}) {
      $Friends{$id}={
        user      =>      undef,
        name      =>      undef,
        bg        =>      undef,
        fg        =>      undef,
  dob   =>  undef,
  birthday  =>  undef,
  groups    =>  [],
  groupmask =>  undef,
        status    =>      "active",
        type      =>      "user",
      };
    }
    if ($type eq "birthday") {
      ($v=~/([0-9]+)-([0-9]{2})-([0-9]{2})/o) || next;
      my @tm=(0,0,0,$3,$2,$1-1900);
      if ($tm[5]>0) {
        $Friends{$id}->{dob}=mktime(@tm);
        if (!defined $Friends{$id}->{dob}) {
          $LJ::Simple::error="Failed to convert time $v into Unix timestamp";
          return undef;
        }
      }
      $tm[5]=(gmtime(time()))[5];
      $Friends{$id}->{birthday}=mktime(@tm);
      if (!defined $Friends{$id}->{birthday}) {
        $LJ::Simple::error="Failed to convert time $v into Unix timestamp";
        return undef;
      }
    } else {
      $Friends{$id}->{$type}=$v;
    }
  }
  if (defined $self->{groups}) {
    my $id=undef;
    foreach $id (values %Friends) {
      (defined $id->{groupmask}) || next;
      foreach (values %{$self->{groups}->{name}}) {
        my $bit=1 << $_->{id};
        if (($id->{groupmask} & $bit) == $bit) {
          push(@{$id->{groups}},$_->{name});
        }
      }
    }
  }
  my @lst=sort {$a->{user} cmp $b->{user}} (values %Friends);
  return ($#lst+1,@lst);
}


=pod

=item $lj->CheckFriends(@groups)

This routine is used to poll the LiveJournal server to see if your friends list
has been updated or not. This routine returns a list. The first item in the
list is a value which holds C<1> if there has been an update
to your friends list and C<0> if not. The second item in the list holds the number
of seconds you must wait before calling C<CheckFriends()> again.
In the event of an error C<undef> is returned in the first item of the list.

The routine can be given an optional list of friends group to check instead of
just looking at all of the friends for the user.

Example code:

  while(1) {
    my ($new_friends,$next_update)=$lj->CheckFriends();
    (defined $new_friends) ||
      die "$0: Failed to check friends - $LJ::Simple::error\n";
    ($new_friends) && print "Friends list updated\n";
    sleep($next_update+1);
  }

=cut
sub CheckFriends($$) {
  my $self=shift;
  my (@groups)=@_;
  my %Event=();
  my %Resp=();
  if ($#groups>-1) {
    if (!defined $self->{groups}) {
      $LJ::Simple::error="Groups not requested at login";
      return 0;
    }
    my $g;
    my $mask=0;
    foreach $g (@groups) {
      if (!exists $self->{groups}->{name}->{$g}) {
        $LJ::Simple::error="Group \"$g\" does not exist";
        return 0;
      }
      $mask=$mask | (1 << $self->{groups}->{name}->{$g}->{id});
    }
    $Event{mask}=$mask;
  }
  if (exists $self->{checkfriends}) {
    $Event{lastupdate}=$self->{checkfriends}->{lastupdate};
    my $currtime=time();
    if ($currtime<$self->{checkfriends}->{interval}) {
      $LJ::Simple::error="Insufficent time left between CheckFriends() call";
      return undef;
    }
  } else {
    $self->{checkfriends}={};
  }
  $self->SendRequest("checkfriends",\%Event,\%Resp) || return undef;
  $self->{checkfriends}->{lastupdate}=$Resp{lastupdate};
  $self->{checkfriends}->{interval}=time() + $Resp{interval};
  return ($Resp{new},$Resp{interval});
}


=pod

=item $lj->GetDayCounts($hash_ref,$journal)

This routine is given a reference to hash which it fills with information
on the journal entries posted to the LiveJournal we are currently associated
with. On success the reference to the hash will be returned. On error
C<undef> is returned.

There is an optional argument - C<$journal> - which can be used to gather this
data for a shared journal the user has access to. If not required then this
value should be C<undef> or an empty string.

The key to the hash is a date, given as seconds since epoch (I<i.e.> C<time_t>)
and the value is the number of entries made on that day. Only dates which have
journal entries made against them will have values in the hash; thus it can be
assumed that if a date is B<not> in the hash then no journal entries were made
on that day.

The hash will be emptied before use.

Example code:

  use POSIX;
  (defined $lj->GetDayCounts(\%gdc_hr,undef))
    || die "$0: Failed to get day counts - $LJ::Simple::error\n";
  
  foreach (sort {$a<=>$b} keys %gdc_hr) {
    printf("%s %03d\n",strftime("%Y/%m/%d",gmtime($_)),$gdc_hr{$_});
  }

=cut
sub GetDayCounts($$$) {
  my $self=shift;
  my ($hr,$journal)=@_;
  $LJ::Simple::error="";
  if (ref($hr) ne "HASH") {
    my $r=ref($hr);
    $LJ::Simple::error="CODE: GetDayCounts() given \"$r\", not a hash reference";
    return undef;
  }
  %{$hr}=();
  my %Event=();
  my %Resp=();
  if ((defined $journal) && ($journal ne "")) {
    $Event{usejournal}=$journal;
  }
  $self->SendRequest("getdaycounts",\%Event,\%Resp) || return undef;
  my ($k,$v);
  while(($k,$v)=each %Resp) {
    ($k=~/([0-9]+)-([0-9]+)-([0-9]+)/o) || next;
    ($v==0) && next;
    my $timet=mktime(0,0,0,$3,$2-1,$1-1900);
    if (!defined $timet) {
      $LJ::Simple::error="Failed to convert date $k into Unix timestamp";
      return undef;
    }
    if (exists $hr->{$timet}) {
      $hr->{$timet}=$hr->{$timet}+$v;
    } else {
      $hr->{$timet}=$v;
    }
  }
  return $hr;
}


=pod

=item $lj->GetFriendGroups($hash_ref)

This routine is given a reference to a hash which it fills with information
on the friends groups the user has defined. On success the reference to the
hash will be returned. On error C<undef> is returned.

The hash key is the id number of the friends group as it is possible to
have multiple friends groups with the same name. Each hash value is a hash
reference which points to the following hash:

  {
    id     => Id of the group; used to create permission masks
    name   => Name of the group
    sort   => Sort order number from 0 to 255
    public => Public group ? 1 for yes, 0 for no
  }

The hash given will be emptied before use.

Example code:

  my %fg=();
  (defined $lj->GetFriendGroups(\%fg)) || 
    die "$0: Failed to get groups - $LJ::Simple::error\n";
  
  my $format="| %-4s | %-2s | %-6s | %-40s |\n";
  my $line=sprintf($format,"","","","");
  $line=~s/\|/+/go;
  $line=~s/ /-/go;
  print $line;
  printf($format,"Sort","Id","Public","Group");
  print $line;
  
  foreach (sort {$fg{$a}->{sort}<=>$fg{$b}->{sort}} keys %fg) {
    my $hr=$fg{$_};
    my $pub="No";
    $hr->{public} && ($pub="Yes");
    printf($format,$hr->{sort},$hr->{id},$pub,$hr->{name});
  }
  
  print $line;

In case you're wondering, the above code outputs something similar to
the following:

  +------+----+--------+------------------------------------------+
  | Sort | Id | Public | Group                                    |
  +------+----+--------+------------------------------------------+
  | 5    | 1  | Yes    | Good Friends                             |
  | 10   | 2  | No     | Communities                              |
  +------+----+--------+------------------------------------------+

=cut
sub GetFriendGroups($$) {
  my $self=shift;
  my ($hr)=@_;
   $LJ::Simple::error="";
  if (ref($hr) ne "HASH") {
    my $r=ref($hr);
    $LJ::Simple::error="CODE: GetFriendGroups() given \"$r\", not a hash reference";
    return undef;
  }
  %{$hr}=();
  my %Event=();
  my %Resp=();
  $self->SendRequest("getfriendgroups",\%Event,\%Resp) || return undef;
  my ($k,$v);
  while(($k,$v)=each %Resp) {
    $k=~/^frgrp_([0-9]+)_(.*)$/o || next;
    my ($id,$name)=($1,$2);
    if (!exists $hr->{$id}) {
      $hr->{$id}={
  id  => $id,
  public  => 0,
      };
    }
    ($name eq "sortorder") && ($name="sort");
    $hr->{$id}->{$name}=$v;
  }
  return $hr;
}

=pod

=item $lj->CreateFriendGroups($hash_ref)

This routine modifies user friend groups according to $hash_ref

=cut

sub CreateFriendGroups($$) {
  my $self=shift;
  my ($hr)=@_;
  $LJ::Simple::error="";
  if (ref($hr) ne "HASH") {
    my $r=ref($hr);
    $LJ::Simple::error="CODE: CreateFriendGroups() given \"$r\", not a hash reference";
    return undef;
  }
  my %Event=();
  my %Resp=();

  my $i = 0;
  foreach my $grpid (keys %{$hr}) {
    if ($grpid > 0 && $grpid < 31) {
      my $pname = "efg_set_" . $grpid . "_name";
      $Event{$pname} = $hr->{$grpid}->{name};
      $i++;
    }
  }

  $self->SendRequest("editfriendgroups",\%Event,\%Resp) || return undef;
  return $i;
}

=pod

=back

=head2 The creation and editing of entries

=over 4

=item $lj->NewEntry($event)

Prepares for a new journal entry to be sent into the LiveJournal system.
Takes a reference to a hash which will be emptied and prepared for use
by the other routines used to prepare a journal entry for posting.

On success returns C<1>, on failure returns C<0>

Example code:

  my %Entry=();
  $lj->NewEntry(\%Entry) 
    || die "$0: Failed to prepare new post - $LJ::Simple::error\n";

=cut
sub NewEntry($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  ## Build the event hash - put defaults in
  my $ltime=time();
  my @ltime=localtime($ltime);
  %{$event}=(
  __new_entry =>  1,
  event   =>  undef,
  lineenddings  =>  "unix",
  subject   =>  undef,
  year    =>  $ltime[5]+1900,
  mon   =>  $ltime[4]+1,
  day   =>  $ltime[3],
  hour    =>  $ltime[2],
  min   =>  $ltime[1],
  __timet   =>  $ltime,
  );
  return 1;
}


=pod 

=item $lj->SetDate($event,$time_t)

Sets the date for the event being built from the given C<time_t> (i.e. seconds
since epoch) value. Bare in mind that you may need to call
C<$lj-E<gt>Setprop_backdate(\%Event,1)> to backdate the journal entry if the journal being
posted to has events more recent than the date being set here. Returns C<1> on
success, C<0> on failure.

If the value given for C<time_t> is C<undef> then the current time is used.
If the value given for C<time_t> is negative then it is taken to be relative
to the current time, i.e. a value of C<-3600> is an hour earlier than the
current time.

Note that C<localtime()> is called to convert the C<time_t> value into
the year, month, day, hours and minute values required by LiveJournal.
Thus the time given to LiveJournal will be the local time as shown on
the machine the code is running on.

Example code:

  ## Set date to current time
  $lj->SetDate(\%Event,undef)
    || die "$0: Failed to set date of entry - $LJ::Simple::error\n";

  ## Set date to Wed Aug 14 11:56:42 2002 GMT
  $lj->SetDate(\%Event,1029326202)
    || die "$0: Failed to set date of entry - $LJ::Simple::error\n";

  ## Set date to an hour ago
  $lj->SetDate(\%Event,-3600)
    || die "$0: Failed to set date of entry - $LJ::Simple::error\n";

=cut
sub SetDate($$$) {
  my $self=shift;
  my ($event,$timet)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  (defined $timet) || ($timet=time());
  if ($timet<0) {
    $timet=time() + $timet;
  }
  my @ltime=localtime($timet);
  $event->{__timet}=$timet;
  $event->{year}=$ltime[5]+1900;
  $event->{mon}=$ltime[4]+1;
  $event->{day}=$ltime[3];
  $event->{hour}=$ltime[2];
  $event->{min}=$ltime[1];
  return 1;
}


=pod

=item $lj->SetMood($event,$mood)

Given a mood this routine sets the mood for the journal entry. Unlike the
more direct C<$lj-E<gt>Setprop_current_mood()> and C<$lj-E<gt>Setprop_current_mood_id(\%Event,)>
routines, this routine will attempt to first attempt to find the mood given
to it in the mood list returned by the LiveJournal server. If it is unable to
find a suitable mood then it uses the text given.

Note that if the LiveJournal
object was created with either C<moods> set to C<0> or
with C<fast> set to C<1> then this function will not attempt to find the
mood name given in C<$mood> in the mood list.

Returns C<1> on success, C<0> otherwise.

Example code:

  $lj->SetMood(\%Event,"happy")
    || die "$0: Failed to set mood - $LJ::Simple::error\n";

=cut
sub SetMood($$$) {
  my $self=shift;
  my ($event,$mood) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  ## Simple opt - none of the mood names have a space in them
  if (($mood!~/\s/)&&(defined $self->{mood_map})) { 
    my $lc_mood=lc($mood);
    if (exists $self->{mood_map}->{$lc_mood}) {
      return $self->Setprop_current_mood_id($event,$self->{mood_map}->{$lc_mood})
    }
  }
  return $self->Setprop_current_mood($event,$mood);
}



=pod

=item $lj->UseJournal($event,$journal)

The journal entry will be posted into the shared journal given
as an argument rather than the default journal for the user.

Returns C<1> on success, C<0> otherwise.

Example code:

  $lj->UseJournal(\%Event,"some_community")
    || die "$0: Failed to - $LJ::Simple::error\n";

=cut
sub UseJournal($$$) {
  my $self=shift;
  my ($event,$journal) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  if ((defined $self->{access})&&(!exists $self->{access}->{$journal})) { 
    $LJ::Simple::error="user unable to post to journal \"$journal\"";
    return 0;
  }
  $event->{usejournal}=$journal;
  return 1;
}


=pod

=item $lj->SetSubject($event,$subject)

Sets the subject for the journal entry. The subject has the following
limitations:

  o Limited to a length of 255 characters
  o No newlines are allowed

Returns C<1> on success, C<0> otherwise.

Example code:

  $lj->SetSubject(\%Event,"Some subject")
    || die "$0: Failed to set subject - $LJ::Simple::error\n";

=cut
sub SetSubject($$$) {
  my $self=shift;
  my ($event,$subject) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  if ($subject) {
    if (length($subject)>255) {
      my $len=length($subject);
      $LJ::Simple::error="Subject length limited to 255 characters [given $len]";
      return 0;
    }
    if ($subject=~/[\r\n]/) {
      $LJ::Simple::error="New lines not allowed in subject";
      return 0;
    }
    $event->{subject}=$subject;
  }
  else {
    $event->{subject}="";
  }
  return 1;
}


=pod

=item $lj->SetEntry($event,@entry)

Sets the entry for the journal; takes a list of strings. It should be noted
that this list will be C<join()>ed together with a newline between each
list entry.

If the list is null or C<undef> then any existing entry is removed.

Returns C<1> on success, C<0> otherwise.

Example code:

  # Single line entry
  $lj->SetEntry(\%Event,"Just a simple entry")
    || die "$0: Failed to set entry - $LJ::Simple::error\n";
  
  # Three lines of text
  my @stuff=(
       "Line 1",
       "Line 2",
       "Line 3",
  );
  $lj->SetEntry(\%Event,@stuff)
    || die "$0: Failed to set entry - $LJ::Simple::error\n";

  # Clear the entry
  $lj->SetEntry(\%Event,undef)
    || die "$0: Failed to set entry - $LJ::Simple::error\n";
  $lj->SetEntry(\%Event)
    || die "$0: Failed to set entry - $LJ::Simple::error\n";

=cut
sub SetEntry($$@) {
  my $self=shift;
  my ($event,@entry) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  if ((!defined $entry[0]) || ($#entry == -1)) {
    $event->{event}=undef;
  } else {
    $event->{event}=join("\n",@entry);
  }
  return 1;
}


=pod

=item $lj->AddToEntry($event,@entry)

Adds a string to the existing journal entry being worked on. The new data
will be appended to the existing entry with a newline separating them.
It should be noted that as with C<$lj-E<gt>SetEntry()> the list given to
this routine will be C<join()>ed together with a newline between each 
list entry.

If C<$lj-E<gt>SetEntry()> has not been called then C<$lj-E<gt>AddToEntry()> acts
in the same way as C<$lj-E<gt>SetEntry()>.

If C<$lj-E<gt>SetEntry()> has already been called then calling C<$lj-E<gt>AddToEntry()>
with a null list or a list which starts with C<undef> is a NOP.

Returns C<1> on success, C<0> otherwise.

Example code:

  # Single line entry
  $lj->AddToEntry(\%Event,"Some more text")
    || die "$0: Failed to set entry - $LJ::Simple::error\n";
  
  # Three lines of text
  my @stuff=(
       "Line 5",
       "Line 6",
       "Line 7",
  );
  $lj->AddToEntry(\%Event,@stuff)
    || die "$0: Failed to set entry - $LJ::Simple::error\n";

=cut
sub AddToEntry($$@) {
  my $self=shift;
  my ($event,@entry) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  if (!defined $event->{event}) {
    if ((!defined $entry[0]) || ($#entry == -1)) {
      $event->{event}=undef;
    } else {
      $event->{event}=join("\n",@entry);
    }
  } else {
    if ((!defined $entry[0]) || ($#entry == -1)) {
      return 1;
    }
    $event->{event}=join("\n",$event->{event},@entry);
  }
  return 1;
}


=pod

=back

=head2 Setting of journal entry security levels

=over 4

=item $lj->SetProtect($event,$type,@args)

A wrapper function which calls the underlying C<SetProtect*()> routines
for the caller. This takes two or more arguments; the first argument is
the hash reference of the current event. The second argument is the
type of security we are setting. Subsequent arguments are related to
the security type. Available types and their arguments are:

  +---------+------------------+------------------------------------+
  |  Type   | Additional args  | Security                           |
  +---------+------------------+------------------------------------+
  | public  |      None        | Public - the default               |
  | friends |      None        | Friends only                       |
  | groups  | A list of groups | Restricted to groups of friends    |
  | private |      None        | Private - only the user can access |
  +---------+------------------+------------------------------------+

On success this routine returns C<1>; otherwise it returns C<0> and
sets C<$LJ::Simple::error> to the reason why.

Example code:

  ## Make entry public (the default)
  $lj->SetProtect(\%Event,"public")
    || die "$0: Failed to make entry public - $LJ::Simple::error\n";
  
  ## Make entry friends only
  $lj->SetProtect(\%Event,"friends")
    || die "$0: Failed to make entry friends only - $LJ::Simple::error\n";
  
  ## Make entry only readable by friends in the groups "close" and "others"
  $lj->SetProtect(\%Event,"groups","close","others")
    || die "$0: Failed to make entry public - $LJ::Simple::error\n";
  
  ## Make entry private so only the journal owner can view it
  $lj->SetProtect(\%Event,"private")
    || die "$0: Failed to make entry private - $LJ::Simple::error\n";

=cut
sub SetProtect($$$@) {
  my $self=shift;
  my ($event,$type,@args)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  if ($type eq "public") {
    return $self->SetProtectPublic($event);
  } elsif ($type eq "friends") {
    return $self->SetProtectFriends($event);
  } elsif ($type eq "groups") {
    return $self->SetProtectGroups($event,@args);
  } elsif ($type eq "private") {
    return $self->SetProtectPrivate($event);
  } else {
    $LJ::Simple::error="CODE: type \"$type\" not recognised by SetProtect()";
    return 0;
  }
};

=pod

=item $lj->SetProtectPublic($event)

Sets the current post so that anyone can read the journal entry. Note that this
is the default for a new post created by C<LJ::Simple> - this method is most
useful when working with an existing post. Returns C<1> on success, C<0>
otherwise.

Example code:

  $lj->SetProtectPublic(\%Event)
    || die "$0: Failed to make entry public - $LJ::Simple::error\n";

=cut
sub SetProtectPublic($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  (exists $event->{security}) && delete $event->{security};
  (exists $event->{allowmask}) && delete $event->{allowmask};
  return 1;
}


=pod


=pod

=item $lj->SetProtectFriends($event)

Sets the current post so that only friends can read the journal entry. Returns
C<1> on success, C<0> otherwise.

Example code:

  $lj->SetProtectFriends(\%Event)
    || die "$0: Failed to protect via friends - $LJ::Simple::error\n";

=cut
sub SetProtectFriends($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  $event->{security}="usemask";
  $event->{allowmask}=1;
  return 1;
}


=pod

=item $lj->SetProtectGroups($event,$group1, $group2, ... $groupN)

Takes a list of group names and sets the current entry so that only those
groups can read the journal entry. Returns
C<1> on success, C<0> otherwise.

Example code:

  $lj->SetProtectGroups(\%Event,"foo","bar")
    || die "$0: Failed to protect via group - $LJ::Simple::error\n";

=cut
sub SetProtectGroups($$@) {
  my $self=shift;
  my ($event,@grps) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  if (!defined $self->{groups}) {
    $LJ::Simple::error="Groups not requested at login";
    return 0;
  }
  if ($#grps==-1) {
    $LJ::Simple::error="No group names given";
    return 0;
  }
  $event->{security}="usemask";
  my $g;
  my $mask=0;
  foreach $g (@grps) {
    if (!exists $self->{groups}->{name}->{$g}) {
      $LJ::Simple::error="Group \"$g\" does not exist";
      return 0;
    }
    $mask=$mask | (1 << $self->{groups}->{name}->{$g}->{id});
  }
  $event->{allowmask}=$mask;
  return 1;
}

=pod

=item $lj->SetProtectPrivate($event)

Sets the current post so that the owner of the journal only can read the
journal entry. Returns C<1> on success, C<0> otherwise.

Example code:

  $lj->SetProtectPrivate(\%Event)
    || die "$0: Failed to protect via private - $LJ::Simple::error\n";

=cut
sub SetProtectPrivate($$) {
  my $self=shift;
  my ($event) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  $event->{security}="private";
  (exists $event->{allowmask}) &&
    delete $event->{allowmask};
  return 1;
}


##
## Helper function used to set meta data
##
sub Setprop_general($$$$$$) {
  my ($self,$event,$prop,$caller,$type,$data)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  my $nd=undef;
  if ($type eq "bool") {
    if (($data == 1)||($data == 0)) {
      $nd=$data;
    } else {
      $LJ::Simple::error="INTERNAL: Invalid value [$data] for type bool [from $caller]";
      return 0;
    }
  } elsif ($type eq "char") {
    $nd=$data;
  } elsif ($type eq "num") {
    if ($data!~/^[0-9]+$/o) {
      $LJ::Simple::error="INTERNAL: Invalid value [$data] for type num [from $caller]";
      return 0;
    }
    $nd=$data;
  } else {
    $LJ::Simple::error="INTERNAL: Unknown type \"$type\" [from $caller]";
    return 0;
  }
  if (!defined $nd) {
    $LJ::Simple::error="INTERNAL: Setprop_general did not set \$nd [from $caller]";
    return 0;
  }
  $event->{"prop_$prop"}=$nd;
  return 1;
}

=pod

=back

=head2 Setting journal entry properties

=over 4

=item $lj->Setprop_taglist($event,@tags)

Set the tags for the entry; C<@tags> is a list of the tags to give the 
entry.

Example code:

  $lj->Setprop_taglist(\%Event,qw( gabe pets whatever )) ||
    die "$0: Failed to set back date property - $LJ::Simple::error\n";

=cut
sub Setprop_taglist($$@) {
  my ($self,$event,@tags)=@_;
  $LJ::Simple::error="";
  return $self->Setprop_general($event,"taglist","Setprop_taglist","char",join(", ",@tags));
}

=pod

=item $lj->Setprop_backdate($event,$onoff)

Used to indicate if the journal entry being written should be back dated or not. Back dated
entries do not appear on the friends view of your journal entries. The C<$onoff>
value takes either C<1> for switching the property on or C<0> for switching the
property off. Returns C<1> on success, C<0> on failure.

You will need to set this value if the journal entry you are sending has a
date earlier than other entries in your journal.

Example code:

  $lj->Setprop_backdate(\%Event,1) ||
    die "$0: Failed to set back date property - $LJ::Simple::error\n";

=cut
sub Setprop_backdate($$$) {
  my ($self,$event,$onoff)=@_;
  $LJ::Simple::error="";
  return $self->Setprop_general($event,"opt_backdated","Setprop_backdate","bool",$onoff);
}


=pod

=item $lj->Setprop_current_mood($event,$mood)

Used to set the current mood for the journal being written. This takes a string which
describes the mood.

It is better to use C<$lj-E<gt>SetMood()> as that will automatically use a
mood known to the LiveJournal server if it can.

Returns C<1> on success, C<0> on failure.

Example code:

  $lj->Setprop_current_mood(\%Event,"Happy, but tired") ||
    die "$0: Failed to set current_mood property - $LJ::Simple::error\n";

=cut
sub Setprop_current_mood($$$) {
  my ($self,$event,$mood)=@_;
  $LJ::Simple::error="";
  if ($mood=~/[\r\n]/) {
    $LJ::Simple::error="Mood may not contain a new line";
    return 0;
  }
  return $self->Setprop_general($event,"current_mood","Setprop_current_mood","char",$mood);
}

=pod

=item $lj->Setprop_current_mood_id($event,$id)

Used to set the current mood_id for the journal being written. This takes a number which
refers to a mood_id the LiveJournal server knows about.

Note that if the LiveJournal
object was created with either C<moods> set to C<0> or
with C<fast> set to C<1> then this function will not attempt to validate
the C<mood_id> given to it.

It is better to use C<$lj-E<gt>SetMood()> as that will automatically use a
mood known to the LiveJournal server if it can.

Returns C<1> on success, C<0> on failure.

Example code:

  $lj->Setprop_current_mood_id(\%Event,15) ||
    die "$0: Failed to set current_mood_id property - $LJ::Simple::error\n";

=cut
sub Setprop_current_mood_id($$$) {
  my ($self,$event,$data)=@_;
  $LJ::Simple::error="";
  if (defined $self->{moods}) {
    if (!exists $self->{moods}->{$data}) {
      $LJ::Simple::error="The mood_id $data is not known by the LiveJournal server";
      return 0;
    }
  }
  return $self->Setprop_general($event,"current_moodid","Setprop_current_mood_id","num",$data);
}


=pod

=item $lj->Setprop_current_music($event,$music)

Used to set the current music for the journal entry being written. This takes
a string.

Returns C<1> on success, C<0> on failure.

Example code:

  $lj->Setprop_current_music(\%Event,"Collected euphoric dance") ||
    die "$0: Failed to set current_music property - $LJ::Simple::error\n";

=cut
sub Setprop_current_music($$$) {
  my ($self,$event,$data)=@_;
  $LJ::Simple::error="";
  return $self->Setprop_general($event,"current_music","Setprop_current_music","char",$data);
}

=pod

=item $lj->Setprop_preformatted($event,$onoff)

Used to set if the text for the journal entry being written is preformatted in HTML
or not. This takes a boolean value of C<1> for true and C<0> for false.

Returns C<1> on success, C<0> on failure.

Example code:

  $lj->Setprop_preformatted(\%Event,1) ||
    die "$0: Failed to set property - $LJ::Simple::error\n";

=cut
sub Setprop_preformatted($$$) {
  my ($self,$event,$data)=@_;
  $LJ::Simple::error="";
  return $self->Setprop_general($event,"opt_preformatted","Setprop_preformatted","bool",$data);
}


=pod

=item $lj->Setprop_nocomments($event,$onoff)

Used to set if the journal entry being written can be commented on or not. This takes
a boolean value of C<1> for true and C<0> for false. Thus if you use a value
of C<1> (true) then comments will not be allowed.

Returns C<1> on success, C<0> on failure.

Example code:

  $lj->Setprop_nocomments(\%Event,1) ||
    die "$0: Failed to set property - $LJ::Simple::error\n";

=cut
sub Setprop_nocomments($$$) {
  my ($self,$event,$data)=@_;
  $LJ::Simple::error="";
  return $self->Setprop_general($event,"opt_nocomments","Setprop_nocomments","bool",$data);
}


=pod

=item $lj->Setprop_picture_keyword($event,$keyword)

Used to set the picture keyword for the journal entry being written. This takes
a string. We check to make sure that the picture keyword exists.

Note that if the LiveJournal
object was created with either C<pics> set to C<0> or
with C<fast> set to C<1> then this function will B<not> validate
the picture keyword before setting it.

Returns C<1> on success, C<0> on failure.

Example code:

  $lj->Setprop_picture_keyword(\%Event,"Some photo") ||
    die "$0: Failed to set property - $LJ::Simple::error\n";

=cut
sub Setprop_picture_keyword($$$) {
  my ($self,$event,$data)=@_;
  $LJ::Simple::error="";
  if (defined $self->{pictures}) {
    my $match=0;
    foreach (values %{$self->{pictures}}) {
      if ($_->{name} eq $data) {
        $match=1;
        last;
      }
    }
    if (!$match) {
      $LJ::Simple::error="Picture keyword not associated with journal";
      return 0;
    }
  }
  return $self->Setprop_general($event,"picture_keyword","Setprop_picture_keyword","char",$data);
}


=pod

=item $lj->Setprop_noemail($event,$onoff)

Used to say that comments on the journal entry being written should not be emailed.
This takes boolean value of C<1> for true and C<0> for false.

Returns C<1> on success, C<0> on failure.

Example code:

  $lj->Setprop_noemail(\%Event,1) ||
    die "$0: Failed to set property - $LJ::Simple::error\n";

=cut
sub Setprop_noemail($$$) {
  my ($self,$event,$data)=@_;
  $LJ::Simple::error="";
  return $self->Setprop_general($event,"opt_noemail","Setprop_noemail","bool",$data);
}


=pod

=item $lj->Setprop_unknown8bit($event,$onoff)

Used say that there is 8-bit data which is not in UTF-8 in the journal entry
being written. This takes a boolean value of C<1> for true and C<0> for false.

Returns C<1> on success, C<0> on failure.

Example code:

  $lj->Setprop_unknown8bit(\%Event,1) ||
    die "$0: Failed to set property - $LJ::Simple::error\n";

=cut
sub Setprop_unknown8bit($$$) {
  my ($self,$event,$data)=@_;
  $LJ::Simple::error="";
  return $self->Setprop_general($event,"unknown8bit","Setprop_unknown8bit","bool",$data);
}


=pod

=back

=head2 Posting, editing and deleting journal entries

=over 4

=item $lj->PostEntry($event)

Submit a journal entry into the LiveJournal system. This requires you to have
set up the journal entry with C<$lj-E<gt>NewEntry()> and to have at least called
C<$lj-E<gt>SetEntry()>.

On success a list containing the following is returned:

  o The item_id as returned by the LiveJournal server
  o The anum as returned by the LiveJournal server
  o The item_id of the posted entry as used in HTML - that is the
    value of C<($item_id * 256) + $anum)>

On failure C<undef> is returned.

  # Build the new entry
  my %Event;
  $lj->NewEntry(\%Event) ||
    die "$0: Failed to create new journal entry - $LJ::Simple::error\n";

  # Set the journal entry
  $lj->SetEntry(\%Event,"foo") ||
    die "$0: Failed set journal entry - $LJ::Simple::error\n";

  # And post it
  my ($item_id,$anum,$html_id)=$lj->PostEntry(\%Event);
  defined $item_id ||
    die "$0: Failed to submit new journal entry - $LJ::Simple::error\n";

=cut
##
## PostEntry - actually submit a journal entry.
##
sub PostEntry($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return undef;
  }
  if (!exists $event->{"__new_entry"}) {
    $LJ::Simple::error="CODE: NewEntry not called";
    return undef;
  }

  ## Blat any key in $event which starts with a double underscore
  map {/^__/ && delete $event->{$_}} (keys %{$event});

  if (!defined $event->{event}) {
    $LJ::Simple::error="CODE: No journal entry set - call SetEntry() or AddToEntry() first";
    return undef;
  }

  ## Blat any entry in $self->{event} with an undef value
  map {defined $event->{$_} || delete $event->{$_}} (keys %{$event});

  ## Finally send the actual request
  my %Resp=();
  $self->SendRequest("postevent",$event,\%Resp) || return undef;

  if (!exists $Resp{itemid}) {
    $LJ::Simple::error="LJ server did not return itemid";
    return undef;
  }
  if (!exists $Resp{anum}) {
    $LJ::Simple::error="LJ server did not return anum";
    return undef;
  }

  return ($Resp{itemid},$Resp{anum},($Resp{itemid} * 256) + $Resp{anum});
}

=pod

=item $lj->EditEntry($event)

Edit an entry from the LiveJournal system which has the givem C<item_id>.
The entry should have been fetched from LiveJournal using the
C<$lj-E<gt>GetEntries()> function and then adjusted using the various
C<$lj-E<gt>Set...()> functions.

It should be noted that this function can be used to delete a journal entry
by setting the entry to a blank string, I<i.e.> by using
C<$lj-E<gt>SetEntry(\%Event,undef)>

Returns C<1> on success, C<0> on failure.

Example:

  # Fetch the most recent event
  my %Events = ();
  (defined $lj->GetEntries(\%Events,undef,"one",-1)) ||
    die "$0: Failed to get entries - $LJ::Simple::error\n";
  
  # Mark it as private
  foreach (values %Entries) {
    $lj->SetProtectPrivate($_);
    $lj->EditEntry($_) ||
      die "$0: Failed to edit entry - $LJ::Simple::error\n";
  }
  
  # Alternatively we could just delete it...
  my $event=(values %Entries)[0];
  $lj->SetEntry($event,undef);
  $lj->EditEntry($event) ||
    die "$0: Failed to edit entry - $LJ::Simple::error\n";

=cut
sub EditEntry($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return 0;
  }
  if (!exists $event->{"__itemid"}) {
    $LJ::Simple::error="CODE: Not an existing entry; use GetEntry()";
    return 0;
  }
  $event->{itemid}=$event->{"__itemid"};

  ## Blat any key in $event which starts with a double underscore
  map {/^__/ && delete $event->{$_}} (keys %{$event});

  if (!defined $event->{event}) {
    $LJ::Simple::error="CODE: No journal entry set";
    return 0;
  }

  ## Blat any entry in $event with an undef value
  map {defined $event->{$_} || delete $event->{$_}} (keys %{$event});

  ## Make the request
  return $self->SendRequest("editevent",$event,undef);
}

=pod

=item $lj->DeleteEntry($item_id)

Delete an entry from the LiveJournal system which has the given C<item_id>.
On success C<1> is returned; on failure C<0> is returned.

Example:

  $lj->DeleteEntry($some_item_id) ||
    die "$0: Failed to delete journal entry - $LJ::Simple::error\n";

=cut
sub DeleteEntry($$) {
  my $self=shift;
  my ($item_id) = @_;
  $LJ::Simple::error="";
  if (!defined $item_id) {
    $LJ::Simple::error="CODE: DeleteEntry() given undefined item_id";
    return 0;
  }
  if ($item_id!~/^[0-9]+$/) {
    $LJ::Simple::error="CODE: DeleteEntry() given invalid item_id";
    return 0;
  }
  my %Event=(
  itemid  =>  $item_id,
  event =>  "",
  );
  return $self->SendRequest("editevent",\%Event,undef);
}

=pod

=back

=head2 Retriving journal entries

=over 4

=item $lj->SyncItems($timestamp)

This routine returns a list of all of the items (journal entries, to-do items,
comments) which have been created or updated on LiveJournal. There is an optional
timestamp value for specifying the time you last synchronised with the server.
This timestamp value can either be a Unix-style C<time_t> value or a previously
returned timestamp from this routine. If not used specify the undefined value
C<undef>.

When specifying the time you must take into account the fact that the modification
or creation times of the entries in the LiveJournal database are stored as the
time local to the computer running the database rather than GMT. Due to this
it is safest to use the time from the latest item downloaded from the LiveJournal
from a previous C<SyncItems()> call.

On success this routine will return a list which contains first the number of
valid items in the list and then a list of hashes which contain the details
of the items found. This routine can return an empty list which signifies that
no new items could be found. On failure C<undef> is returned.

The format of the returned list is as follows. The list of hashes is ordered
by the timestamps of the entries, oldest to newest.

  @list = (
    number of items returned,
    {
      item_id   => Item_id of the entry changed
      type      => Type of entry
      action    => What happened to the entry
      time_t    => Time of change in Unix time (see note below)
      timestamp => Timestamp from server
    },
  );

The C<type> of entry can be one of the following letters:

  L: Journal entries
  C: Comments
  T: To-do items

It should be noted that currently the LiveJournal system will only ever
return C<L> types due to the C<C> and C<T> types not having been implemented
in the LiveJournal code yet.

The C<action> of the entry can be either C<create> for a new entry,
C<update> for an entry which has been modified or C<del> for a deleted entry.

The C<time_t> value is probably going to be wrong; as far as the author of
this code can tell, you can not get the timezone of the server which is
serving out the request. This means that converting the timestamps
returned by the server from their format of C<YYYY-MM-DD hh:mm:ss> into
a Unix C<time_t> value is inaccurate at best since C<time_t> is defined
as the number of seconds since 00:00 1st January 1970 B<GMT>. Functions
like C<mktime()> which can be used to create C<time_t> values have to
assume that the data they are being given is valid for the timezone the
machine it is running on is actually in. Given the nature of the net
this is rarely the case. I<sigh> I wish that the LJ developers had stored
timestamps in pure C<time_t> in the database... and if they have done they
should provide a way for developers to get access to this as its B<much>
more useful IMHO.

Given the above you're probably wondering why I included the C<time_t>
value. Well, whilst the value isn't much use when it really comes down
to it, it B<is> useful when it comes to sorting the list of entries as
all of the entries from the same server will be inaccurate to the same
amount.

The C<timestamp> from server takes the format of C<YYYY-MM-DD hh:mm:ss>

It should be noted that this routine can take a long time to return
if there are large numbers of entries to be returned. This is especially
true if you give C<undef> as the timestamp.

Example code:

  # All entries in the last day or so; this is fudged due to timezone
  # differences (WTF didn't they store stuff in GMT ?)
  my ($num_of_items,@lst)=$lj->SyncItems(time() - (86400 * 2));
  
  (defined $num_of_items) ||
    die "$0: Failed to sync - $LJ::Simple::error\n";

  my $hr=undef;
  print "Number of items: $num_of_items\n";
  print "Item_id\tType\tAction\tTime_t\t\tTimestamp\n";
  foreach $hr (@lst) {
    print "$hr->{item_id}\t" .
          "$hr->{type}\t" .
          "$hr->{action}\t" .
          "$hr->{time_t}\t" .
          "$hr->{timestamp}\n";
  }

There is also an example of how to work with all of the entries of a LiveJournal
shown in the C<examples/friends-only> script which accompanies the C<LJ::Simple>
distribution. This example script looks at a LiveJournal and makes sure that every
journal entry is at the very least marked as being friends-only.

=cut
sub SyncItems($$) {
  my $self=shift;
  my ($timet)=@_;
  $LJ::Simple::error="";
  if ($LJ::Simple::debug) {
    my $ts=undef;
    if (defined $timet) {
      $ts="\"$timet\"";
    } else {
      $ts="undef";
    }
    Debug "SyncItems($ts)";
  }
  my %Event=();
  my %Resp=();
  if (defined $timet) {
    if ($timet=~/^[0-9]+$/) {
      my @tm=gmtime($timet);
      if ($#tm==-1) {
        $LJ::Simple::error="CODE: Invalid timestamp";
        return undef;
      }
      $Event{lastsync}=strftime("%Y-%m-%d %H:%M:%S",@tm);
    } else {
      $Event{lastsync}=$timet;
    }
  }
  $self->SendRequest("syncitems",\%Event,\%Resp) || return undef;
  my %Mh=();
  my $sync_count;
  my $sync_total;
  my $latest=0;
  my $latest_ts;
  my ($key,$val);
  while(($key,$val)=each %Resp) {
    if ($key=~/sync_([0-9]+)_(.*)$/o) {
      my ($id,$name)=($1,$2);
      (exists $Mh{$id}) || ($Mh{$id}={});
      if ($name eq "item") {
        my ($type,$item_id)=split(/-/,$val,2);
        $Mh{$id}->{item_id}=$item_id;
        $Mh{$id}->{type}=$type;
      } elsif ($name eq "action") {
        $Mh{$id}->{action}=$val;
      } elsif ($name eq "time") {
        $Mh{$id}->{timestamp}=$val;
        if ($val!~/([0-9]+)-([0-9]+)-([0-9]+)\s([0-9]+):([0-9]+):([0-9]+)/io) {
          $LJ::Simple::error="INTERNAL: failed to parse timestamp \"$val\"";
          return undef;
        }
        $Mh{$id}->{time_t}=mktime($6,$5,$4,$3,$2-1,$1-1900,0,0,0);
        if (!defined $Mh{$id}->{time_t}) {
          $LJ::Simple::error="INTERNAL: failed to create time_t from \"$val\"";
          return undef;
        }
        if ($Mh{$id}->{time_t}>$latest) {
          $latest_ts=$val;
          $latest=$Mh{$id}->{time_t};
        }
      } else {
        $LJ::Simple::error="INTERNAL: Unrecognised sync_[0-9]_* \"$key\"";
        return undef;
      }
    } elsif ($key eq "sync_total") {
      $sync_total=$val;
    } elsif ($key eq "sync_count") {
      $sync_count=$val;
    }
  }
  Debug "sync_count=$sync_count\n";
  Debug "sync_total=$sync_total\n";
  my @lst=();
  push(@lst,values %Mh);
  if ($sync_count != $sync_total) {
    my ($num,@nl)=$self->SyncItems($latest_ts);
    (defined $num) || return undef;
    push(@lst,@nl);
  }
  @lst=sort { $a->{time_t} <=> $b->{time_t} } @lst;
  map { $_->{kv}=join(":",$_->{item_id},$_->{type},$_->{action},$_->{time_t}) } @lst;
  my %seen=();
  @lst=grep((!exists $seen{$_->{kv}}) && ($seen{$_->{kv}}=1),@lst);
  my $tot=$#lst+1;
  return ($tot,@lst);
}

=pod

=item $lj->GetEntries($hash_ref,$journal,$type,@opt)

This routine allows you to pull events from the user's LiveJournal. There are
several different ways this routine can work depending on the value given in
the C<$type> argument.

This routine will currently only allow you to get a B<maximum of 50 journal entries>
thanks to restrictions imposed by LiveJournal servers. If you want to perform work
on I<every> journal entry within a LiveJournal account then you should look at the
C<SyncItems()> routine documented above.

The first argument - C<$hash_ref> is a reference to a hash which will be filled
with the details of the journal entries downloaded. The key to this hash is the
C<item_id> of the journal entries. The value is a hash reference which points to
a hash of the same type created by C<NewPost()> and used by C<PostEntry()> and
C<EditEntry()>. The most sensible way to access this hash is to use the various
C<Get*()> routines.

The second argument - C<$journal> - is an optional argument set if the journal
to be accessed is a shared journal. If this is set then the name of shared journal
will be propogated into the entries returned in the hash reference C<$hash_ref> as
if C<$lj->UseJournal($event,$journal)> was called. If not required set this to C<undef>.

The third argument - C<$type> - specifies how the journal entries are to be
pulled down. The contents of the fourth argument - C<@opt> - will depend on the
value in the C<$type> variable. Thus:

  +-------+------------+------------------------------------------+
  | $type | @opt       | Comments                                 |
  +-------+------------+------------------------------------------+
  | day   | $timestamp | Download a single day. $timestamp is a   |
  |       |            | Unix timestamp for the required day      |
  +-------+------------+------------------------------------------+
  | lastn |$num,$before| Download a number of entries. $num has a |
  |       |            | maximum value of 50. If $num is undef    |
  |       |            | then the default of 20 is used. $before  |
  |       |            | is an optional value which specifies a   |
  |       |            | date before which all entries must occur.|
  |       |            | The date is specified as a Unix          |
  |       |            | timestamp. If not specified the value    |
  |       |            | should be undef.                         |
  +-------+------------+------------------------------------------+
  | one   | $item_id   | The unique ItemID for the entry to be    |
  |       |            | downloaded. A value of -1 means to       |
  |       |            | download the most recent entry           |
  +-------+------------+------------------------------------------+
  | sync  | $date      | Get journal entries since the given date.|
  |       |            | The date should be specified as a Unix   |
  |       |            | timestamp.                               |
  +-------+------------+------------------------------------------+

If the operation is successful then C<$hash_ref> is returned. On failure
C<undef> is returned and C<$LJ::Simple::error> is updated with the
reason for the error.

Example code:

The following code only uses a single C<$type> from the above list; C<one>.
However the hash of hashes returned is the same in every C<$type> used. The
code below shows how to pull down the last journal entry posted and then uses
all of the various C<Get*()> routines to decode the hash returned.

  use POSIX;
  
  my %Entries=();
  (defined $lj->GetEntries(\%Entries,undef,"one",-1)) ||
    die "$0: Failed to get entries - $LJ::Simple::error\n";
  
  my $Entry=undef;
  my $Format="%-20s: %s\n";

  foreach $Entry (values %Entries) {
  
    # Get URL
    my $url=$lj->GetURL($Entry);
    (defined $url) && print "$url\n";
  
    # Get ItemId
    my ($item_id,$anum,$html_id)=$lj->GetItemId($Entry);
    (defined $item_id) && printf($Format,"Item_id",$item_id);
  
    # Get the subject
    my $subj=$lj->GetSubject($Entry);
    (defined $subj) && printf($Format,"Subject",$subj);
  
    # Get the date entry was posted
    my $timet=$lj->GetDate($Entry);
    if (defined $timet) {
      printf($Format,"Date",
             strftime("%Y-%m-%d %H:%M:%S",gmtime($timet)));
    }
  
    # Is entry protected ?
    my $EntProt="";
    my ($protect,@prot_opt)=$lj->GetProtect($Entry);
    if (defined $protect) {
      if ($protect eq "public") {
         $EntProt="public";
      } elsif ($protect eq "friends") {
        $EntProt="friends only";
      } elsif ($protect eq "groups") {
        $EntProt=join("","only groups - ",join(", ",@prot_opt));
      } elsif ($protect eq "private") {
        $EntProt="private";
      }
      printf($Format,"Journal access",$EntProt);
    }
  
    ## Properties
    # Backdated ?
    my $word="no";
    my $prop=$lj->Getprop_backdate($Entry);
    if ((defined $prop) && ($prop==1)) { $word="yes" }
    printf($Format,"Backdated",$word);
  
    # Preformatted ?
    $word="no";
    $prop=$lj->Getprop_preformatted($Entry);
    if ((defined $prop) && ($prop==1)) { $word="yes" }
    printf($Format,"Preformatted",$word);
  
    # No comments allowed ?
    $word="no";
    $prop=$lj->Getprop_nocomments($Entry);
    if ((defined $prop) && ($prop==1)) { $word="yes" }
    printf($Format,"No comments",$word);
  
    # Do not email comments ?
    $word="no";
    $prop=$lj->Getprop_noemail($Entry);
    if ((defined $prop) && ($prop==1)) { $word="yes" }
    printf($Format,"No emailed comments",$word);
  
    # Unknown 8-bit ?
    $word="no";
    $prop=$lj->Getprop_unknown8bit($Entry);
    if ((defined $prop) && ($prop==1)) { $word="yes" }
    printf($Format,"Any 8 bit, non UTF-8",$word);
  
    # Current music
    $word="[None]";
    $prop=$lj->Getprop_current_music($Entry);
    if ((defined $prop) && ($prop ne "")) { $word=$prop }
    printf($Format,"Current music",$word);
  
    # Current mood [text]
    $word="[None]";
    $prop=$lj->Getprop_current_mood($Entry);
    if ((defined $prop) && ($prop ne "")) { $word=$prop }
    printf($Format,"Current mood",$word);
  
    # Current mood [id]
    $word="[None]";
    $prop=$lj->Getprop_current_mood_id($Entry);
    if ((defined $prop) && ($prop ne "")) { $word=$prop }
    printf($Format,"Current mood_id",$word);
  
    # Picture keyword
    $word="[None]";
    $prop=$lj->Getprop_picture_keyword($Entry);
    if ((defined $prop) && ($prop ne "")) { $word=$prop }
    printf($Format,"Picture keyword",$word);
  
    # Finally output the actual journal entry
    printf($Format,"Journal entry","");
    my $text=$lj->GetEntry($Entry);
    (defined $text) &&
      print "  ",join("\n  ",split(/\n/,$text)),"\n\n";
  }

=cut
sub GetEntries($$@) {
  my $self=shift;
  my ($hr,$journal,$type,@opts)=@_;
  $LJ::Simple::error="";
  if (ref($hr) ne "HASH") {
    $LJ::Simple::error="CODE: GetEntries() not given a hash reference";
    return undef;
  }
  %{$hr}=();
  my %Event=();
  my %Resp=();
  if (defined $journal) {
    $Event{usejournal}=$journal;
  }
  my $ctype=lc($type);
  if ($ctype eq "day") {
    if ($#opts<0) {
      $LJ::Simple::error="CODE: GetEntries($type) requires year,month,day in \@opts";
      return undef;
    }
    my ($timestamp)=@opts;
    if ($timestamp!~/^[0-9]+$/) {
      $LJ::Simple::error="CODE: GetEntries($type) given invalid timestamp";
      return undef;
    }
    my @tm=localtime($timestamp);
    if ($#tm==-1) {
      $LJ::Simple::error="CODE: GetEntries($type) given invalid timestamp";
      return undef;
    }
    $Event{selecttype}=$ctype;
    $Event{year}=$tm[5]+1900;
    $Event{month}=$tm[4]+1;
    $Event{day}=$tm[3];
  } elsif ($ctype eq "lastn") {
    if ($#opts<1) {
      $LJ::Simple::error="CODE: GetEntries($type) requires num and beforedate in \@opts";
      return undef;
    }
    $Event{selecttype}=$ctype;
    my ($num,$beforedate)=@opts;
    if (defined $num) {
      if ($num!~/^[0-9]{1,2}$/) {
        $LJ::Simple::error="CODE: GetEntries($type) requires valid number for num";
        return undef;
      }
      if ($num>50) {
        $LJ::Simple::error="Maximum number of journal entries returned is 50";
        return undef;
      }
    } else {
      $num=20;
    }
    $Event{howmany}=$num;
    if (defined $beforedate) {
      if ($beforedate!~/^[0-9]+$/) {
        $LJ::Simple::error="Invalid Unix timestamp";
        return undef;
      }
      my @tm=gmtime($beforedate);
      if ($#tm==-1) {
        $LJ::Simple::error="CODE: GetEntries($type) given invalid timestamp";
        return undef;
      }
      $Event{beforedate}=strftime("%Y-%m-%d %H:%M:%S",@tm);
    }
  } elsif ($ctype eq "one") {
    if ($#opts<0) {
      $LJ::Simple::error="CODE: GetEntries($type) requires item_id in \@opts";
      return undef;
    }
    my ($item_id)=@opts;
    if ($item_id!~/^-*[0-9]+$/) {
      $LJ::Simple::error="Invalid item_id";
      return undef;
    }
    if ($item_id<-1) {
      $LJ::Simple::error="Invalid item_id";
      return undef;
    }
    $Event{selecttype}=$ctype;
    $Event{itemid}=$item_id;
  } elsif ($ctype eq "sync") {
    if ($#opts<0) {
      $LJ::Simple::error="CODE: GetEntries($type) requires timestamp in \@opts";
      return undef;
    }
    my ($lastsync)=@opts;
    if ($lastsync!~/^[0-9]+$/) {
      $LJ::Simple::error="Invalid Unix timestamp";
      return undef;
    }
    my @tm=gmtime($lastsync);
    if ($#tm==-1) {
      $LJ::Simple::error="CODE: GetEntries($type) given invalid timestamp";
      return undef;
    }
    $Event{lastsync}=strftime("%Y-%m-%d %H:%M:%S",@tm);
    $Event{selecttype}="syncitems";
  } else {
    $LJ::Simple::error="CODE: GetEntries() does not understand type $type\n";
    return undef;
  }
  $self->SendRequest("getevents",\%Event,\%Resp) || return undef;
  my %Ev=();
  my %Pr=();
  my ($k,$v);
  while(($k,$v)=each %Resp) {
    my ($num,$key,$hash)=(undef,undef,undef);
    if ($k=~/^events_([0-9]+)_(.*)$/) {
      ($num,$key,$hash)=($1,$2,\%Ev);
    } elsif ($k=~/^prop_([0-9]+)_(.*)$/) {
      ($num,$key,$hash)=($1,$2,\%Pr);
    }
    if (defined $hash) {
      (exists $hash->{$num}) || ($hash->{$num}={});
      $hash->{$num}->{$key}=$v;
    }
  }
  my $ehr=undef;
  foreach $ehr (values %Ev) {
    my $itemid=$ehr->{itemid};
    $hr->{$itemid}={};
    my $nhr=$hr->{$itemid};
    %{$nhr}=(
      __htmlid    =>  ($ehr->{itemid} * 256) + $ehr->{anum},
      __anum    =>  $ehr->{anum},
      __itemid    =>  $itemid,
      event   =>  $ehr->{event},
      lineenddings  =>  "unix",
    );
    (defined $journal) && ($nhr->{usejournal}=$journal);
    (exists $ehr->{subject}) && ($nhr->{subject}=$ehr->{subject});
    (exists $ehr->{allowmask}) && ($nhr->{allowmask}=$ehr->{allowmask});
    (exists $ehr->{security}) && ($nhr->{security}=$ehr->{security});
    if ($ehr->{eventtime}=~/([0-9]+)-([0-9]+)-([0-9]+)\s([0-9]+):([0-9]+):([0-9]+)/o) {
      $nhr->{year}=int($1);
      $nhr->{mon}=int($2);
      $nhr->{day}=int($3);
      $nhr->{hour}=int($4);
      $nhr->{min}=int($5);
      my $timet=mktime($6,$5,$4,$3,$2-1,$1-1900);
      if (!defined $timet) {
        $LJ::Simple::error="Failed to mktime() from \"$ehr->{eventtime}\" for itemid $hr->{$ehr->{itemid}}->{__htmlid}";
        return undef;
      }
      $nhr->{__timet}=$timet;
    } else {
      $LJ::Simple::error="Failed to parse eventtime \"$ehr->{eventtime}\" for itemid $hr->{$ehr->{itemid}}->{__htmlid}";
      return undef;
    }
  }
  my $phr=undef;
  foreach $phr (values %Pr) {
    if (!exists $hr->{$phr->{itemid}}) {
      $LJ::Simple::error="Protocol error: properties returned for itemid not seen";
      return undef;
    }
    my $nhr=$hr->{$phr->{itemid}};
    my $k=join("_","prop",$phr->{name});
    if (!exists $nhr->{$k}) {
      $nhr->{$k}=$phr->{value};
    }
  }
  return $hr;
}

=pod 

=back

=head2 Getting information from an entry

=over 4

=item $lj->GetDate($event)

Gets the date for the event given. The date is returned as a C<time_t> (i.e. seconds
since epoch) value. Returns C<undef> on failure.

Example code:

  use POSIX;  # For strftime()
  
  ## Get date
  my $timet=$lj->GetDate(\%Event);
  (defined $timet)
    || die "$0: Failed to set date of entry - $LJ::Simple::error\n";
  
  # Get time list using gmtime()
  my @tm=gmtime($timet);
  ($#tm<0) &&
    die "$0: Failed to run gmtime() on time_t $timet\n";
  
  # Format date in the normal way used by LJ "YYYY-MM-DD hh:mm:ss"
  my $jtime=strftime("%Y-%m-%d %H:%M:%S",@tm);

=cut
sub GetDate($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return undef;
  }
  if (!exists $event->{__timet}) {
    $LJ::Simple::error="No time value stored";
    return undef;
  }
  return $event->{__timet};
}


=pod

=item $lj->GetItemId($event)

Returns a list which contains the real C<item_id>, C<anum> and HTMLised C<item_id> which
can be used to contruct a URL suitable for accessing the item via the web.
Returns C<undef> on failure. Note that you must only use this
routine on entries which have been returned by the C<GetEntries()>
routine.

Example code:

  my ($item_id,$anum,$html_id)=$lj->GetItemId(\%Event);
  (defined $item_id)
    || die "$0: Failed to get item id - $LJ::Simple::error\n";

=cut
sub GetItemId($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return undef;
  }
  if (!exists $event->{__itemid}) {
    $LJ::Simple::error="item_id does not exist - must use GetEntries()";
    return undef;
  }
  if (!exists $event->{__anum}) {
    $LJ::Simple::error="anum does not exist - must use GetEntries()";
    return undef;
  }
  if (!exists $event->{__htmlid}) {
    $LJ::Simple::error="HTML id does not exist - must use GetEntries()";
    return undef;
  }
  return ($event->{__itemid},$event->{__anum},$event->{__htmlid});
}


=pod

=item $lj->GetURL($event)

Returns the URL which can be used to access the journal entry via a web
browser. Returns C<undef> on failure. Note that you must only use this
routine on entries which have been returned by the C<GetEntries()>
routine.

Example code:

  my $url=$lj->GetURL(\%Event);
  (defined $url)
    || die "$0: Failed to get URL - $LJ::Simple::error\n";
  system("netscape -remote 'openURL($url)'");

=cut
sub GetURL($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return undef;
  }
  if (!exists $event->{__htmlid}) {
    $LJ::Simple::error="HTML id does not exist - must use GetEntries()";
    return undef;
  }
  my $user=$self->user();
  my $server=$self->{lj}->{host};
  my $port=$self->{lj}->{port};
  my $htmlid=$event->{__htmlid};
  return "http://$server:$port/talkpost.bml\?journal=$user\&itemid=$htmlid";
}

=pod

=item $lj->GetSubject($event)

Gets the subject for the journal entry. Returns the subject if it is
available, C<undef> otherwise.

Example code:

  my $subj=$lj->GetSubject(\%Event)
  if (defined $subj) {
    print "Subject: $subj\n";
  }

=cut
sub GetSubject($$) {
  my $self=shift;
  my ($event) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return undef;
  }
  if (!exists $event->{subject}) {
    $LJ::Simple::error="No subject set";
    return undef;
  }
  return $event->{subject};
}


=pod

=item $lj->GetEntry($event)

Gets the entry for the journal. Returns either a single string which contains
the entire journal entry or C<undef> on failure.

Example code:

  my $ent = $lj->GetEntry(\%Event);
  (defined $ent)
    || die "$0: Failed to get entry - $LJ::Simple::error\n";
  print "Entry: $ent\n";

=cut
sub GetEntry($$) {
  my $self=shift;
  my ($event) = @_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return undef;
  }
  if (!exists $event->{event}) {
    $LJ::Simple::error="No journal entry set";
    return undef;
  }
  return $event->{event};
}


=pod

=item $lj->GetProtect($event)

Gets the protection information on the event given. Returns a list with
details of the protection set on the post. On failure C<undef> is returned.

There are several different types of protection which can be returned for a
journal entry. These include public, friends only, specific friends groups
and private. The list returned will always have the type of protection listed
first followed by any details of that protection. Thus the list can contain:

  ("public")
    A publically accessable journal entry
  
  ("friends")
    Only friends may read the entry
    
  ("groups","group1" ...)
    Only users listed in the friends groups given after the "groups"
    may read the entry
  
  ("private")
    Only the owner of the journal may read the entry

Example code:

  my ($protect,@prot_opt)=$lj->GetProtect(\%Event);
  (defined $protect) ||
    die "$0: Failed to get entry protection type - $LJ::Simple::error\n";
  if ($protect eq "public") {
    print "Journal entry is public\n";
  } elsif ($protect eq "friends") {
    print "Journal entry only viewable by friends\n"; 
  } elsif ($protect eq "groups") {
    print "Journal entry only viewable by friends in the following groups:\n";
    print join(", ",@prot_opt),"\n";
  } elsif ($protect eq "private") {
    print "Journal entry only viewable by the journal owner\n"; 
  }

=cut
sub GetProtect($$) {
  my $self=shift;
  my ($event)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return undef;
  }
  if ((!exists $event->{security})||($event->{security} eq "")) {
    return "public";
  }
  if ($event->{security} eq "private") {
    return "private";
  }
  if ($event->{security} ne "usemask") {
    $LJ::Simple::error="INTERNAL: security contains unknown value \"$event->{security}\"";
    return undef;
  }
  if (($event->{allowmask} & 1) == 1) {
    return "friends";
  }
  my @lst=("groups");
  my $g=undef;
  foreach $g (keys %{$self->{groups}->{name}}) {
    my $bit=1 << $self->{groups}->{name}->{$g}->{id};
    if (($event->{allowmask} & $bit) == $bit) {
      push(@lst,$g);
    }
  }
  return @lst;
}


##
## Helper function used to get meta data
##
sub Getprop_general($$$$$) {
  my ($self,$event,$prop,$caller,$type)=@_;
  $LJ::Simple::error="";
  if (ref($event) ne "HASH") {
    $LJ::Simple::error="CODE: Not given a hash reference";
    return undef;
  }
  my $key=join("_","prop",$prop);
  if (!exists $event->{$key}) {
    if ($type eq "bool") {
      return 0;
    }
    return "";
  }
  return $event->{$key};
}

=pod

=item $lj->Getprop_taglist($event)

Get tags for given entry.

=cut
sub Getprop_taglist($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"taglist","Getprop_taglist","char");
}

=pod

=item $lj->Getprop_backdate($event)

Indicates if the journal entry is back dated or not. Back dated
entries do not appear on the friends view of your journal entries. Returns
C<1> if the entry is backdated, C<0> if it is not. C<undef> is returned in the
event of an error.

Example code:

  my $prop=$lj->Getprop_backdate(\%Event);
  (defined $prop) ||
    die "$0: Failed to get property - $LJ::Simple::error\n";
  if ($prop) {
    print STDERR "Journal is backdated\n";
  } else {
    print STDERR "Journal is not backdated\n";
  }
  

=cut
sub Getprop_backdate($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"opt_backdated","Getprop_backdate","bool");
}


=pod

=item $lj->Getprop_current_mood($event)

Used to get the current mood for the journal being written. This returns the
mood if one exists, an empty string if none exists or C<undef> in the event
of an error.

Example code:

  my $prop=$lj->Getprop_current_mood(\%Event);
  (defined $prop) ||
    die "$0: Failed to get property - $LJ::Simple::error\n";
  if ($prop ne "") {
    print STDERR "Journal has mood of $prop\n";
  } else {
    print STDERR "Journal has no mood set\n";
  }


=cut
sub Getprop_current_mood($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"current_mood","Getprop_current_mood","char");
}

=pod

=item $lj->Getprop_current_mood_id($event)

Used to get the current mood_id for the journal being written. Will return
the mood_id if one is set, a null string is one is not set and C<undef> in
the event of an error.

Example code:

  my $prop=$lj->Getprop_current_mood_id(\%Event);
  (defined $prop) ||
    die "$0: Failed to get property - $LJ::Simple::error\n";
  if ($prop ne "") {
    print STDERR "Journal has mood_id of $prop\n";
  } else {
    print STDERR "Journal has no mood_id set\n";
  }


=cut
sub Getprop_current_mood_id($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"current_moodid","Getprop_current_mood_id","num");
}


=pod

=item $lj->Getprop_current_music($event)

Used to get the current music for the journal entry being written. Returns
the music if one is set, a null string is one is not set and C<undef> in
the event of an error.

Example code:

  my $prop=$lj->Getprop_current_music(\%Event);
  (defined $prop) ||
    die "$0: Failed to get property - $LJ::Simple::error\n";
  if ($prop) {
    print STDERR "Journal has the following music: $prop\n";
  } else {
    print STDERR "Journal has no music set for it\n";
  }

=cut
sub Getprop_current_music($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"current_music","Getprop_current_music","char");
}

=pod

=item $lj->Getprop_preformatted($event)

Used to see if the text for the journal entry being written is preformatted in HTML
or not. This returns true (C<1>) if so, false (C<0>) if not.

Example code:

  $lj->Getprop_preformatted(\%Event) &&
    print "Journal entry is preformatted\n";

=cut
sub Getprop_preformatted($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"opt_preformatted","Getprop_preformatted","bool");
}


=pod

=item $lj->Getprop_nocomments($event)

Used to see if the journal entry being written can be commented on or not.
This returns true (C<1>) if so, false (C<0>) if not.

Example code:

  $lj->Getprop_nocomments(\%Event) &&
    print "Journal entry set to disallow comments\n";

=cut
sub Getprop_nocomments($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"opt_nocomments","Getprop_nocomments","bool");
}


=pod

=item $lj->Getprop_picture_keyword($event)

Used to get the picture keyword for the journal entry being written. Returns
the picture keyword if one is set, a null string is one is not set and C<undef> in
the event of an error.

Example code:

  my $prop=$lj->Getprop_picture_keyword(\%Event);
    (defined $prop) ||
    die "$0: Failed to get property - $LJ::Simple::error\n";
  if ($prop) {
    print STDERR "Journal has picture keyword $prop set\n";
  } else {
    print STDERR "Journal has no picture keyword set\n";
  }


=cut
sub Getprop_picture_keyword($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"picture_keyword","Getprop_picture_keyword","char");
}


=pod

=item $lj->Getprop_noemail($event)

Used to see if comments on the journal entry being written should be emailed or
not. This returns true (C<1>) if so comments should B<not> be emailed and false
(C<0>) if they should be emailed.

Example code:

  $lj->Getprop_noemail(\%Event) &&
    print "Comments to journal entry not emailed\n";

=cut
sub Getprop_noemail($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"opt_noemail","Getprop_noemail","bool");
}


=pod

=item $lj->Getprop_unknown8bit($event)

Used see if there is 8-bit data which is not in UTF-8 in the journal entry
being written. This returns true (C<1>) if so, false (C<0>) if not.

Example code:

  $lj->Getprop_unknown8bit(\%Event) &&
    print "Journal entry contains 8-bit data not in UTF-8 format\n";

=cut
sub Getprop_unknown8bit($$) {
  my ($self,$event)=@_;
  $LJ::Simple::error="";
  return $self->Getprop_general($event,"unknown8bit","Getprop_unknown8bit","bool");
}



##### Start of helper functions

##
## A helper function which takes a key and value pair;
## both are encoded for HTTP transit.
##
sub EncVal($$) {
  my ($key,$val)=@_;
  $key=~s/ /\+/go;
  $key=~s/([^a-z0-9+])/sprintf("%%%x",ord($1))/egsi;
  $val=~s/ /\+/go;
  $val=~s/([^a-z0-9+])/sprintf("%%%02x",ord($1))/egsi;
  return "$key=$val";
}

##
## A helper function which takes an encoded value from HTTP
## transit and decodes it
##
sub DecVal($) {
  my ($val)=@_;
  $val=~s/\+/ /go;
  $val=~s/%([0-9A-F]{2})/pack("C", hex($1))/egsi;
  return "$val";
}

##
## Actually make the LJ request; could be called directly, but isn't
## documented.
##
## The first argument is the the mode to use. The list of currently
## supported modes is:
##  o login
##  o postevent
##
## The second argument is a hash reference to arguments specific to the
## mode.
##
## The third argument is a reference to a hash which contain the response
## from the LJ server. This can be undef.
##
## Returns 1 on success, 0 on failure. On failure $LJ::Simple::error is
## populated.
##
sub SendRequest($$$$) {
  my ($self,$mode,$args,$req_hash)=@_;
  my ($oself, $omode, $oargs, $oreq_hash, $oretry) = @_;
  $oretry = 0 unless defined($oretry);

  $LJ::Simple::error="";
  my $sub=$LJ::Simple::NonBlock;
  my $bytes_in=0;
  my $bytes_out=0;
  my $timestart=time();
  if ((defined $sub) && (ref($sub) ne "CODE")) { 
    my $reftype=ref($sub);
    $LJ::Simple::error="\$LJ::Simple::NonBlock given a $reftype reference, not CODE";
    return 0;
  }
  $self->{request}={};
  if ((ref($args) ne "HASH")&&($mode ne "getchallenge")) {
    $LJ::Simple::error="INTERNAL: SendRequest() not given hashref for arguments";
    return 0;
  }
  if ((defined $req_hash) && (ref($req_hash) ne "HASH")) {
    $LJ::Simple::error="INTERNAL: SendRequest() not given hashref for responses";
    return 0;
  }
  $mode=lc($mode);
  my @request=(
  "mode=$mode",
  );
  if ($mode ne "getchallenge") {
    push(@request,
    EncVal("user",$self->{auth}->{user}),
  );
    # Much fun here - see if we use the challenge-response stuff
    if ($LJ::Simple::challenge) {
      Debug("Trying to use challenge-response system");
      Debug("  Getting new challenge");
      my %chall=();
      $self->SendRequest("getchallenge",undef,\%chall) || return 0;
      if ($chall{auth_scheme} ne "c0") {
        $LJ::Simple::error="Server returned unsupported auth_scheme \"$chall{auth_scheme}\"";
        return 0;
      }
      Debug("    Got challenge from server:");
      Debug("        challenge: $chall{challenge}");
      Debug("      expire_time: $chall{expire_time}");
      Debug("      server_time: $chall{server_time}");

      # Work out our own timeout point, basically the livetime of the
      # challenge less 10 seconds of fudge factor.
      my $chall_livetime=$chall{expire_time} - $chall{server_time} - 10;
      my $ctime=time();
      $self->{auth}->{challenge}->{timeout}=$ctime + $chall_livetime;
      Debug("    Challenge lifetime is $chall_livetime seconds");
      Debug("      Current: $ctime");
      Debug("       Expire: $self->{auth}->{challenge}->{timeout}");

      $self->{auth}->{challenge}->{challenge}=$chall{challenge};
      # We assume that the Digest::MD5 module is loaded already; also
      # means that we have an MD5 hash of the password to hand.
      my $md5=Digest::MD5->new;
      $md5->add($chall{challenge});
      $md5->add($self->{auth}->{hash});
      $self->{auth}->{challenge}->{hash}=$md5->hexdigest;
    }
    if (exists $self->{auth}->{challenge}->{hash}) {
      push(@request,
    EncVal("auth_method","challenge"),
    EncVal("auth_challenge",$self->{auth}->{challenge}->{challenge}),
    EncVal("auth_response",$self->{auth}->{challenge}->{hash}),
  );
    } else {
      if (exists $self->{auth}->{hash}) {
        push(@request,EncVal("hpassword",$self->{auth}->{hash}));
      } else {
        push(@request,EncVal("password",$self->{auth}->{pass}));
      }
    }
    my $ljprotver=0;
    if ($LJ::Simple::UTF) { $ljprotver=1; }
    push(@request,
  "ver=$ljprotver",
    );
  }
  (defined $sub) && &{$sub}($mode,0.1,"Preparing request data",$bytes_in,$bytes_out,time()-$timestart,0);
  if ($mode eq "login") {
    push(@request,EncVal("clientversion",$LJ::Simple::LJ_Client));
    if ((exists $args->{moods}) && ($args->{moods} == 1)) {
      push(@request,EncVal("getmoods",0));
    }
    if ((exists $args->{getpickws}) && ($args->{getpickws} == 1)) {
      push(@request,EncVal("getpickws",1));
      push(@request,EncVal("getpickwurls",1));
    }
  } elsif ( ($mode eq "postevent")
         || ($mode eq "editevent") 
         || ($mode eq "syncitems") 
         || ($mode eq "getevents") 
         || ($mode eq "getfriends") 
         || ($mode eq "friendof") 
         || ($mode eq "checkfriends") 
         || ($mode eq "getdaycounts") 
         || ($mode eq "getfriendgroups") 
         || ($mode eq "editfriendgroups") 
          ) {
    if (defined $args) {
      my ($k,$v);
      while(($k,$v)=each %{$args}) {
        if (!defined $k) {
          $LJ::Simple::error="CODE: SendRequest() given undefined key value";
          return 0;
        }
        if (!defined $v) {
          $LJ::Simple::error="CODE: SendRequest() given undefined value for \"$k\"";
          return 0;
        }
        push(@request,EncVal($k,$v));
      }
    }
  } elsif ( ($mode eq "getchallenge")
      || ($mode eq "sessiongenerate") 
    ) {
    # NOP - nothing required
  } elsif ($mode eq "getrawdata") {
    if (! $args->{url}) {
      $LJ::Simple::error="CODE: SendRequest() given undefined url value with getrawdata;";
      return 0;
    }
    if (! $self->{auth}->{ljsession}) {
      $LJ::Simple::error="CODE: SendRequest(getrawdata) called without previous sessiongenerate;";
      return 0;
    }
  } else {
    $LJ::Simple::error="INTERNAL: SendRequest() given unsupported mode \"$mode\"";
    return 0;
  }
  my $req=join("&",@request);
  my $ContLen=length($req);

  (defined $sub) && &{$sub}($mode,0.2,"Preparing connection to server",$bytes_in,$bytes_out,time()-$timestart,0);

  ## Now we've got the request ready, time to start talking to the web
  # Work out where we're talking to and the URI to do it with
  my $server=$self->{lj}->{host};
  my $host=$server;
  my $port=$self->{lj}->{port};
  
  my $uri="/interface/flat";
  $uri = $args->{url} if $mode eq "getrawdata";

  if (defined $self->{proxy}) {
    $uri="http://$server:$port$uri";
    $server=$self->{proxy}->{host};
    $port=$self->{proxy}->{port};
  }

  # Prepare the HTTP request now we've got the URI
  my @HTTP = ();
  if ($mode eq "getrawdata") {

    my $post_data = "user=" . $self->{auth}->{user} . "&auth_method=cookie";

    if ($args->{"post-data"}) {
      foreach (keys %{$args->{"post-data"}}) {
        $post_data .= "&" . $_ . "=" . $args->{"post-data"}->{$_};
      }
    }
    my $method = "POST";
    if ($args->{"http_method"}) {
      $method = $args->{"http_method"};
    }

    @HTTP=(
    "$method $uri HTTP/1.0",
    "Host: $host",
    "Content-type: application/x-www-form-urlencoded",
    "User-Agent: " . $LJ::Simple::UserAgent,
    "Content-length: " . length($post_data),
    "X-LJ-Auth: cookie",
    "Cookie: ljsession=" . $self->{auth}->{ljsession},
    );

    push(@HTTP,
      "",
      $post_data,
      "",
    );
  }
  else {
    @HTTP=(
    "POST $uri HTTP/1.0",
    "Host: $host",
    "Content-type: application/x-www-form-urlencoded",
    "User-Agent: " . $LJ::Simple::UserAgent,
    "Content-length: $ContLen",
    );
    if ($self->{fastserver}) {
      push(@HTTP,"Cookie: ljfastserver=1");
    }
    push(@HTTP,
    "",
    $req,
    "",
    );
  }

  # Prepare the socket
  my $tcp_proto=getprotobyname("tcp");
  socket(SOCK,PF_INET,SOCK_STREAM,$tcp_proto);

  # Resolve the server name we're connecting to
  (defined $sub) && &{$sub}($mode,0.3,"Starting to resolve $server to IP address",$bytes_in,$bytes_out,time()-$timestart,0);
  my $addr=inet_aton($server);
  if (!defined $addr) {
    if ($oretry < $LJ::Simple::network_retries) {
      $oretry++;
      sleep $LJ::Simple::network_sleep;
      return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
    }
    else {
      $LJ::Simple::error="Failed to resolve server $server";
      return 0;
    }
  }
  my $sin=sockaddr_in($port,$addr);

  my $ip_addr=join(".",unpack("CCCC",$addr));

  my $proto=$LJ::Simple::ProtoSub;
  ($LJ::Simple::protocol) && &{$proto}(undef,undef,$server,$ip_addr);
  if ($LJ::Simple::raw_protocol) {
    print STDERR "Connecting to $server [$ip_addr]\n";
    print STDERR "Lines starting with \"-->\" is data SENT to the server\n";
    print STDERR "Lines starting with \"<--\" is data RECEIVED from the server\n";
  }

  # Connect to the server
  (defined $sub) && &{$sub}($mode,0.4,"Trying to connect to server $server",$bytes_in,$bytes_out,time()-$timestart,0);
  if (!connect(SOCK,$sin)) {
    if ($oretry < $LJ::Simple::network_retries) {
      $oretry++;
      sleep $LJ::Simple::network_sleep;
      return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
    }
    else {
      $LJ::Simple::error="Failed to connect to $server - $!";
      return 0;
    }
  }

  ($LJ::Simple::protocol) && &{$proto}(undef,"Connected to $server [$ip_addr]",$server,$ip_addr);
  ($LJ::Simple::raw_protocol) &&
     print STDERR "Connected to $server [$ip_addr]\n";

  # Send the HTTP request
  (defined $sub) && &{$sub}($mode,0.5,"Starting to send HTTP request to $server",$bytes_in,$bytes_out,time()-$timestart,0);
  my $cp=0.5;
  foreach (@HTTP) {
    my $line="$_\r\n";
    my $len=length($line);
    my $pos=0;
    my $fail=0;
    while($pos!=$len) {
      my $nbytes=syswrite(SOCK,$line,$len,$pos);
      if (!defined $nbytes) {
  if ( ($! == EAGAIN) || ($! == EINTR) ) {
          $fail++;
          if ($fail>4) {
            $LJ::Simple::error="Write to socket failed with EAGAIN/EINTR $fail times";
            shutdown(SOCK,2);
            close(SOCK);
            return 0;
          }
          next;
        } else {
          $LJ::Simple::error="Write to socket failed - $!";
          shutdown(SOCK,2);
          close(SOCK);
          return 0;
        }
      }
      $pos+=$nbytes;
      $bytes_out+=$nbytes;
      $cp=$cp+0.001;
      (defined $sub) && &{$sub}($mode,$cp,"Sending HTTP request to $server",$bytes_in,$bytes_out,time()-$timestart,0);
    }
    ($LJ::Simple::protocol) && &{$proto}(0,$_,$server,$ip_addr);
    ($LJ::Simple::raw_protocol) && print STDERR "--> $_\n";
  }

  # Read the response from the server - use select()
  (defined $sub) && &{$sub}($mode,0.6,"Getting HTTP response from $server",$bytes_in,$bytes_out,time()-$timestart,0);
  $cp=0.6001;
  my ($rin,$rout,$eout)=("","","");
  vec($rin,fileno(SOCK),1) = 1;
  my $ein = $rin;
  my $response="";
  my $done=0;
  while (!$done) {
    my $nfound;
    if (defined $sub) {
      $nfound = select($rout=$rin,undef,$eout=$ein,0);
      my $ttaken=time()-$timestart;
      if ($nfound!=1) {
        if ($ttaken>$LJ::Simple::timeout) {
          &{$sub}($mode,1,"Connection with server $server timed out",$bytes_in,$bytes_out,$ttaken,0);
          $LJ::Simple::error="Failed to receive data from $server [$ip_addr]";
          shutdown(SOCK,2);
          close(SOCK);

          if ($oretry < $LJ::Simple::network_retries) {
            $oretry++;
            sleep $LJ::Simple::network_sleep;
            return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
          }
          else {
            return 0;
          }
        }
        &{$sub}($mode,$cp,"Waiting for response from $server",$bytes_in,$bytes_out,time()-$timestart,1);
        next;
      }
    } else {
      $nfound = select($rout=$rin,undef,$eout=$ein,$LJ::Simple::timeout);
      if ($nfound!=1) {
        $LJ::Simple::error="Failed to receive data from $server [$ip_addr]";
        shutdown(SOCK,2);
        close(SOCK);

        if ($oretry < $LJ::Simple::network_retries) {
          $oretry++;
          sleep $LJ::Simple::network_sleep;
          return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
        }
        else {
          return 0;
        }
      }
    }
    my $resp="";
    my $nbytes=sysread(SOCK,$resp,$LJ::Simple::buffer);

    if (!defined $nbytes) {
      $LJ::Simple::error="Error in getting data from $server [$ip_addr] - $!";
      shutdown(SOCK,2);
      close(SOCK);
      (defined $sub) && &{$sub}($mode,1,$LJ::Simple::error,$bytes_in,$bytes_out,time()-$timestart,0);
      
      if ($oretry < $LJ::Simple::network_retries) {
        $oretry++;
        sleep $LJ::Simple::network_sleep;
        return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
      }
      else {
        return 0;
      }
    } elsif ($nbytes==0) {
      $done=1;
    } else { 
      $bytes_in=$bytes_in+$nbytes;
      (defined $sub) && &{$sub}($mode,$cp,"Getting response from server $server",$bytes_in,$bytes_out,time()-$timestart,0);
      $cp=$cp+0.001;
      $response="$response$resp";
      if ($LJ::Simple::raw_protocol) {
        print STDERR "<-- ";
        foreach (split(//,$resp)) {
          s/([\x00-\x20\x7f-\xff])/sprintf("\\%o",ord($1))/ei;
          print "$_";
        }
        print STDERR "\n";
      } elsif ($LJ::Simple::protocol) {
        foreach (split(/[\r\n]{1,2}/o,$resp)) {
          &{$proto}(1,$_,$server,$ip_addr);
        }
      }
    }
  }
  (defined $sub) && &{$sub}($mode,0.7,"Finished getting data from server $server",$bytes_in,$bytes_out,time()-$timestart,0);
  
  # Shutdown the socket
  if (!shutdown(SOCK,2)) {
    if ($oretry < $LJ::Simple::network_retries) {
      $oretry++;
      sleep $LJ::Simple::network_sleep;
      return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
    }
    else {
      $LJ::Simple::error="Failed to shutdown socket - $!";
      (defined $sub) && &{$sub}($mode,1,$LJ::Simple::error,$bytes_in,$bytes_out,time()-$timestart,0);
      return 0;
    }
  }

  # Close the socket
  close(SOCK);

  (defined $sub) && &{$sub}($mode,0.8,"Parsing data from server $server",$bytes_in,$bytes_out,time()-$timestart,0);
  ## We've got the response from the server, so we now parse it
  if (!defined $response) {
    if ($oretry < $LJ::Simple::network_retries) {
      $oretry++;
      sleep $LJ::Simple::network_sleep;
      return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
    }
    else {
      $LJ::Simple::error="Failed to get result from server";
      return 0;
    }
  }

  ## Ensure that response isn't zero length
  if (length($response) == 0) {
    if ($oretry < $LJ::Simple::network_retries) {
      $oretry++;
      sleep $LJ::Simple::network_sleep;
      return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
    }
    else {
      $LJ::Simple::error="Zero length response from server";
      (defined $sub) && &{$sub}($mode,1,"$LJ::Simple::error $server",$bytes_in,$bytes_out,time()-$timestart,0);
      return 0;
    }
  }

  # Split into headers and body
  my ($http,$body)=split(/\r\n\r\n/,$response,2);

  if (!defined $http) {
    if ($oretry < $LJ::Simple::network_retries) {
      $oretry++;
      sleep $LJ::Simple::network_sleep;
      return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
    }
    else {
      $LJ::Simple::error="Failed to get HTTP headers from server";
      (defined $sub) && &{$sub}($mode,1,"$LJ::Simple::error $server",$bytes_in,$bytes_out,time()-$timestart,0);
      return 0;
    }
  }
  
  if (!defined $body) {
    if ($oretry < $LJ::Simple::network_retries) {
      $oretry++;
      sleep $LJ::Simple::network_sleep;
      return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
    }
    else {
      $LJ::Simple::error="Failed to get HTTP body from server";
      (defined $sub) && &{$sub}($mode,1,"$LJ::Simple::error $server",$bytes_in,$bytes_out,time()-$timestart,0);
      return 0;
    }
  }

  # First lets see if we got a valid response
  $self->{request}->{http}={};
  $self->{request}->{http}->{headers}=[(split(/\r\n/,$http))];
  my $srv_resp=$self->{request}->{http}->{headers}->[0];
  $srv_resp=~/^HTTP\/[^\s]+\s([0-9]+)\s+(.*)/;
  my ($srv_code,$srv_msg)=($1,$2);
  $self->{request}->{http}->{code}=$srv_code;
  $self->{request}->{http}->{msg}=$srv_msg;
  if ($srv_code != 200) {
    $LJ::Simple::error="HTTP request failed with $srv_code $srv_msg";
    return 0;
  }

  if ($mode eq "getrawdata") {
    return {"content" => $body};
  }
  else {
    # We did, so lets pull in the LJ stuff for processing
    $self->{request}->{lj}={};

    # The response from LJ takes the form of a key\nvalue\n
    # Note that the value can be null tho
    $done=0;
    while (!$done) {
      if ($body=~/^([^\n]+)\n([^\n]*)\n(.*)$/so) {
        my ($k,$v)=(undef,undef);
        ($k,$v,$body)=(lc($1),DecVal($2),$3);
        $v=~s/\r\n/\n/go;
        $self->{request}->{lj}->{$k}=$v;
      } else {
        $done=1;
      }
    }

    # Got it into a hash - lets see if we made a successful request
    if ( (!exists $self->{request}->{lj}->{success}) ||
         ($self->{request}->{lj}->{success} ne "OK") ) {

      if ($omode ne "login" && $oretry < $LJ::Simple::network_retries) {
        $oretry++;
        sleep $LJ::Simple::network_sleep;
        return $oself->SendRequest($omode, $oargs, $oreq_hash, $oretry);
      }
      else {
        my $errmsg="Server Error, try again later";
        if (exists $self->{request}->{lj}->{errmsg}) {
          $errmsg=$self->{request}->{lj}->{errmsg};
        }
        $LJ::Simple::error="($mode) LJ request failed: $errmsg";
        (defined $sub) && &{$sub}($mode,1,"$LJ::Simple::error $server",$bytes_in,$bytes_out,time()-$timestart,0);
        return 0;
      }
    }

    # We did!
    # Now to populate the hash we were given (if asked to)
    if (defined $req_hash) {
      %{$req_hash}=();
      my ($k,$v);
      while(($k,$v)=each %{$self->{request}->{lj}}) {
        $req_hash->{$k}=$v;
      }
    }
  }

  (defined $sub) && &{$sub}($mode,1,"Finished processing request to server $server",$bytes_in,$bytes_out,time()-$timestart,0);
  return 1;
}

sub GenerateCookie() {
  my ($self) = @_;

  $self->SendRequest("sessiongenerate", {}, undef) || return undef;
  $self->{auth}->{ljsession} = $self->{request}->{lj}->{ljsession};

  return undef unless $self->{auth}->{ljsession};
  return $self->{auth}->{ljsession};
}

sub GetRawData() {
  my ($self,$args)=@_;

  return $self->SendRequest("getrawdata", $args, undef) || return undef;
}

##
## Output debugging info
##
sub Debug(@) {
  ($LJ::Simple::debug) || return;
  my $msg=join("",@_);
  foreach (split(/\n/,$msg)) {
    print STDERR "DEBUG> $_\n";
  }
}


##
## Dump out a list recursively. Will call dump_hash
## for any hash references in the list.
##
## Generally used for debugging
##
sub dump_list($$) {
  my ($lr,$sp)=@_;
  my $le="";
  my $res="";
  foreach $le (@{$lr}) {
    if (ref($le) eq "HASH") {
      $res="$res$sp\{\n";
      $res=$res . dump_hash($le,"$sp  ");
      $res="$res$sp},\n";
    } elsif (ref($le) eq "ARRAY") {
      $res="$res$sp\[\n" . dump_list($le,"$sp  ") . "$sp],\n";
    } else {
      my $lv=$le;
      if (defined $lv) {
        $lv=~s/\n/\\n/go;
        $lv=quotemeta($lv);
        $lv=~s/\\-/-/go;
        $lv="\"$lv\"";
      } else {
        $lv="undef";
      }
      $res="$res$sp$lv,\n";
    }
  }
  return $res;
}

##
## Dump out a hash recursively. Will call dump_list
## for any list references in the hash values.
##
## Generally used for debugging
##
sub dump_hash($$) {
  my ($hr,$sp)=@_;
  my ($k,$v)=();
  my $res="";
  while(($k,$v)=each %{$hr}) {
    $k=quotemeta($k);
    $k=~s/\\-/-/go;
    if (ref($v) eq "HASH") {
      $res="$res$sp\"$k\"\t=> {\n";
      $res=$res . dump_hash($v,"$sp  ");
      $res="$res$sp},\n";
    } elsif (ref($v) eq "ARRAY") {
      $res="$res$sp\"$k\"\t=> \[\n" . dump_list($v,"$sp  ") . "$sp],\n";
    } else {
      if (defined $v) {
        $v=~s/\n/\\n/go;
        $v=quotemeta($v);
        $v=~s/\\\\n/\\n/go;
        $v=~s/\\-/-/go;
        $v="\"$v\"";
      } else {
        $v="undef";
      }
      my $out="$sp\"$k\"\t=> $v,";
      $res="$res$out\n";
    }
  }
  return $res;
}

1;
__END__

=pod

=back

=head1 AUTHOR

Simon Burr E<lt>simes@bpfh.netE<gt>

=head1 SEE ALSO

perl
L<http://www.livejournal.com/>

=head1 LICENSE

Copyright (c) 2002, Simon Burr E<lt>F<simes@bpfh.net>E<gt>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

  * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer. 
  * Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution. 
  * Neither the name of the author nor the names of its contributors may
    be used to endorse or promote products derived from this software
    without specific prior written permission. 

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

=cut
