#!/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 () { 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 () { 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; }