288 lines
7.3 KiB
Perl
Executable File
288 lines
7.3 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
# -*-perl-*-
|
|
#
|
|
# LiveJournal Sync Client
|
|
# (see http://www.livejournal.com/)
|
|
# (protocol information at http://www.livejournal.com/developer/)
|
|
#
|
|
# Brad Fitzpatrick
|
|
# bradfitz@bradfitz.com
|
|
#
|
|
# For this to work, make a ~/.livejournal.rc file like:
|
|
#
|
|
# user: username
|
|
# password: password
|
|
# syncdir: /path/to/syncdir/
|
|
#
|
|
# use_proxy: 1 (optional)
|
|
# proxy_host: my.proxy.com (optional)
|
|
# proxy_port: 81 (optional)
|
|
#
|
|
# And, if not using livejournal.com's servers:
|
|
#
|
|
# server_host: ...
|
|
# server_port: ...
|
|
# server_uri: ...
|
|
|
|
use strict;
|
|
|
|
my $VERSION = "0.1";
|
|
my $SERVER_HOST = "www.livejournal.com";
|
|
my $SERVER_PORT = 80;
|
|
my $SERVER_URI = "/cgi-bin/log.cgi";
|
|
|
|
##########################################################
|
|
|
|
use URI::Escape;
|
|
use LWP::UserAgent;
|
|
|
|
# load the ~/.livejournal.rc file
|
|
my %rc = ();
|
|
load_rc_file(\%rc);
|
|
$rc{'server_host'} ||= $SERVER_HOST;
|
|
$rc{'server_port'} ||= $SERVER_PORT;
|
|
$rc{'server_uri'} ||= $SERVER_URI;
|
|
|
|
unless ($rc{'user'}) {
|
|
die "Error: No username (user) specified in ~/.livejournal.rc\n";
|
|
}
|
|
unless ($rc{'password'}) {
|
|
die "Error: No password specified in ~/.livejournal.rc\n";
|
|
}
|
|
unless ($rc{'syncdir'}) {
|
|
die "Error: No sync directory specified in ~/.livejournal.rc\n";
|
|
}
|
|
|
|
unless (-d $rc{'syncdir'}) {
|
|
die "Sync dir does not exist ($rc{'syncdir'})\n";
|
|
}
|
|
unless (-w $rc{'syncdir'}) {
|
|
die "Sync dir is not writable ($rc{'syncdir'})\n";
|
|
}
|
|
|
|
print "Starting sync.\n";
|
|
my %last;
|
|
if (open (LAST, "$rc{'syncdir'}/lastsyncs.dat")) {
|
|
print "lastsync file opened.\n";
|
|
while (<LAST>) {
|
|
chomp;
|
|
if (/^(\w+):\s*(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)/) {
|
|
$last{$1} = $2;
|
|
}
|
|
}
|
|
close LAST;
|
|
} else {
|
|
print "no lastsync file found (first use?)\n";
|
|
}
|
|
|
|
my $ua = make_lj_agent();
|
|
|
|
my $get_sync_items = 1;
|
|
|
|
while ($get_sync_items)
|
|
{
|
|
print "Getting some sync items...\n";
|
|
print " last{L} = $last{'L'}\n";
|
|
my %ljres = lj_request($ua, {
|
|
"mode" => "syncitems",
|
|
"user" => $rc{'user'},
|
|
"password" => $rc{'password'},
|
|
"lastsync" => $last{'L'},
|
|
});
|
|
|
|
if ($ljres{'success'} eq "OK")
|
|
{
|
|
print "Got $ljres{'sync_count'} sync items of $ljres{'sync_total'} total.\n";
|
|
if ($ljres{'sync_count'} == $ljres{'sync_total'}) {
|
|
print "Done getting sync items!\n";
|
|
$get_sync_items = 0;
|
|
}
|
|
|
|
my %syncitem;
|
|
|
|
for (my $i=1; $i<=$ljres{'sync_count'}; $i++) {
|
|
next unless ($ljres{"sync_${i}_item"} =~ /^(.+?)-(\d+)/);
|
|
my $type = $1;
|
|
my $id = $2;
|
|
my $time = $ljres{"sync_${i}_time"};
|
|
$syncitem{$type}->{$id} = $time;
|
|
}
|
|
|
|
# currently there's only type "L" (journal entries), but
|
|
# this later could be "C"omment or "T"odo, etc..
|
|
foreach my $type (keys %syncitem)
|
|
{
|
|
print "Syncing type: $type\n";
|
|
|
|
# journal entries
|
|
if ($type eq "L") {
|
|
|
|
# rebuild journal entry here ($entry{$itemid}->{$key} = $val)
|
|
# before writing it to disk.
|
|
my %entry;
|
|
|
|
# keep track of the most recent journal entry saved,
|
|
# but only use in the case when there are no left to
|
|
# find the minimum from. (for sending "lastsync")
|
|
my $maxtime = "0000-00-00 00:00:00";
|
|
|
|
# we want to keep fetching more items until no more of
|
|
# this type exist (we'll delete the key after we write
|
|
# it to disk)
|
|
while (keys %{$syncitem{$type}})
|
|
{
|
|
print "Getting a batch of log entries...\n";
|
|
print " keys = ", scalar(keys %{$syncitem{$type}}), "\n";
|
|
print " lastsync = $last{'L'}\n";
|
|
|
|
my %sres = lj_request($ua, {
|
|
"mode" => "getevents",
|
|
"selecttype" => "syncitems",
|
|
"user" => $rc{'user'},
|
|
"password" => $rc{'password'},
|
|
"lastsync" => $last{'L'},
|
|
});
|
|
|
|
if ($sres{"success"} ne "OK") {
|
|
die "getevents failed: $sres{'errmsg'}\n";
|
|
}
|
|
|
|
# these next two loops reconstruct the journal entry
|
|
# from the response
|
|
for (my $i=1; $i<=$sres{'events_count'}; $i++) {
|
|
my $itemid = $sres{"events_${i}_itemid"};
|
|
$entry{$itemid} = {
|
|
'itemid' => $itemid,
|
|
'eventtime' => $sres{"events_${i}_eventtime"},
|
|
'event' => $sres{"events_${i}_event"},
|
|
'security' => $sres{"events_${i}_security"},
|
|
'allowmask' => $sres{"events_${i}_allowmask"},
|
|
};
|
|
}
|
|
for (my $i=1; $i<=$sres{'prop_count'}; $i++) {
|
|
my $itemid = $sres{"prop_${i}_itemid"};
|
|
my $prop = $sres{"prop_${i}_name"};
|
|
my $value = $sres{"prop_${i}_value"};
|
|
$entry{$itemid}->{"prop_$prop"} = $value;
|
|
}
|
|
|
|
# now, write each journal entry to disk, then erase
|
|
# its from the $syncitem{'L'} hash
|
|
print "Writing journal entries to disk...\n";
|
|
print "Wrote: ";
|
|
foreach my $itemid (sort { $a <=> $b } keys %entry)
|
|
{
|
|
if (open(E, ">$rc{'syncdir'}/$itemid.entry")) {
|
|
foreach (sort keys %{$entry{$itemid}}) {
|
|
print E "$_: $entry{$itemid}->{$_}\n";
|
|
}
|
|
close E;
|
|
print "$itemid, ";
|
|
|
|
# increment maxtime if this sync item was newer.
|
|
if ($syncitem{'L'}->{$itemid} gt $maxtime) {
|
|
$maxtime = $syncitem{'L'}->{$itemid};
|
|
}
|
|
delete $entry{$itemid};
|
|
delete $syncitem{'L'}->{$itemid};
|
|
} else {
|
|
die "Couldn't open $itemid.entry for write!\n";
|
|
}
|
|
}
|
|
print "\n";
|
|
|
|
# now that's stuff written to disk, we need to update
|
|
# the $last{'L'} time
|
|
print "Find new LastL...\n";
|
|
if (keys %{$syncitem{'L'}}) {
|
|
# find the earliest that isn't yet synced
|
|
my @times = sort values %{$syncitem{'L'}};
|
|
$last{'L'} = $times[0];
|
|
print "New LastL (keys) = $last{'L'}\n";
|
|
} else {
|
|
$last{'L'} = $maxtime; # FIXME: subtract a second
|
|
# in case two entries were on
|
|
# same second.
|
|
print "New LastL (maxtime) = $last{'L'}\n";
|
|
}
|
|
if (open (LAST, ">>$rc{'syncdir'}/lastsyncs.dat")) {
|
|
print LAST "L: $last{'L'}\n";
|
|
close LAST;
|
|
} else {
|
|
die "Couldn't append lastsyncs.dat file.\n";
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
die "Error getting sync items: $ljres{'errmsg'}\n";
|
|
}
|
|
}
|
|
print "DONE!\n";
|
|
|
|
sub load_rc_file
|
|
{
|
|
my $rcref = shift;
|
|
my $file = "$ENV{'HOME'}/.livejournal.rc";
|
|
return unless (-e $file);
|
|
open (RC, $file);
|
|
while (<RC>)
|
|
{
|
|
s/^\s+//;
|
|
s/\s+$//;
|
|
next unless /\S/;
|
|
my ($var, $val) = split(/\s*:\s*/, $_);
|
|
$rcref->{$var} = $val;
|
|
}
|
|
close RC;
|
|
}
|
|
|
|
sub make_lj_agent
|
|
{
|
|
my $ua = new LWP::UserAgent;
|
|
$ua->agent("PerlLiveJournalClient/$VERSION");
|
|
$ua->timeout(10);
|
|
return $ua;
|
|
}
|
|
|
|
sub lj_request
|
|
{
|
|
my $ua = shift;
|
|
my $vars = shift;
|
|
my %ljres = ();
|
|
|
|
# Create a request
|
|
my $req = new HTTP::Request POST => "http://$SERVER_HOST:$SERVER_PORT/$SERVER_URI";
|
|
$req->content_type('application/x-www-form-urlencoded');
|
|
$req->content(request_string($vars));
|
|
|
|
# Pass request to the user agent and get a response back
|
|
my $res = $ua->request($req);
|
|
|
|
# Check the outcome of the response
|
|
if ($res->is_success) {
|
|
%ljres = split(/\n/, $res->content);
|
|
} else {
|
|
$ljres{'success'} = "FAIL";
|
|
$ljres{'errmsg'} = "Client error: Error contacing server.";
|
|
}
|
|
return %ljres;
|
|
}
|
|
|
|
sub request_string
|
|
{
|
|
my ($vars) = shift;
|
|
my $req = "";
|
|
foreach (sort keys %{$vars})
|
|
{
|
|
my $val = uri_escape($vars->{$_},"\+\=\&");
|
|
$val =~ s/ /+/g;
|
|
$req .= "&" if $req;
|
|
$req .= "$_=$val";
|
|
}
|
|
return $req;
|
|
}
|