Introducing lj_lockdown 0.0.1

Warning! Danger! Warning! Drama outbreak in sector 7! Lockdown! Lockdown!

It recently came to my attention that LJ provides no means for retroactively locking old post (or if they do, they hide it very, very well). So last night and this morning I hacked up lj_lockdown.rb, a quick and dirty journal-locking script. Give it your login and it will convert all public entries to friends-only. All other entries – private, or with custom visibility – will remain unchanged.

lj_lockdown takes the following command-line parameters:

-u
Reverse the operation, so that friends-only posts are made public
-y
Skip the “are-you-sure” question
-t
Test mode; no changes will be made.
-l USERNAME
Specifies your LJ username. You will be prompted if this is not supplied.
-p PASSWORD
Specifies your LJ password. You will be prompted if this is not supplied.
-d
Turn on debug logging

You will need Ruby to run lj_lockdown.

UPDATE: My web host is being a bitch. Source code is behind the cut. Warning: this is quick-and-dirty code; it’s not very pretty.


/usr/bin/env ruby

require 'logger'
require 'rubygems'
require 'xmlrpc/client'
require 'digest/md5'
require 'pp'
require_gem 'highline'
require_gem 'usage'
require_gem 'progressbar'
require_gem 'facets'

include Digest

usage = Usage.new "[-u] [-d] [-l username] [-p password] [-y] [-t]"
unlock = usage.dash_u
debug = usage.dash_d
confirm = usage.dash_y
username = usage.username
password = usage.password
test_mode = usage.dash_t

$log = Logger.new($stdout)

$stderr.sync = true

if debug
  $log.level = Logger::DEBUG
else
  $log.level = Logger::INFO
end

class LiveJournal
  PAUSE_SECONDS = 1

  def initialize(username, password)
    @username = username
    @password = password
    @server = XMLRPC::Client.new 'livejournal.com', '/interface/xmlrpc'
    @last_call_time = nil
  end

  def method_missing(meth, args)
    args = args.dup
    if @last_call_time and (Time.now - @last_call_time) < PAUSE_SECONDS
    $log.debug("Sleeping to give server a break")
      sleep(Time.now - @last_call_time)
    end
    @last_call_time = Time.now
    challenge = @server.call('LJ.XMLRPC.getchallenge')['challenge']
    sleep PAUSE_SECONDS
    response = MD5.hexdigest(challenge + MD5.hexdigest(@password))
    args['username'] = @username
    args['auth_method'] = 'challenge'
    args['auth_challenge'] = challenge
    args['auth_response'] = response
    args['ver'] = 1
    @server.call('LJ.XMLRPC.' + meth.to_s,args)
  end
end

# helper predicates
def is_friends_only?(post)
  (post['security'] == 'usemask') and 
    ((post['allowmask'] & 0x1) == 1)
end

def is_public?(post)
 (post['security'] == nil)
end

begin
  user = HighLine.new

  username ||= user.ask "Please enter your LJ username: "
  password ||= user.ask("Please enter your LJ password: ") {|q|
    q.echo = '*'
  }
  exit unless confirm or user.agree "Are you sure you want to make all" +
    (unlock ? " friends-only posts public?" : " public posts friends-only?")

  if test_mode then $log.info "Test mode; no changes will be made." end

  lj = LiveJournal.new(username, password)

  $log.debug "Getting list of posts"
  $log.debug "Finding out the most recent post"
  events = lj.getevents('truncate' => 4, 'noprops' => 1, 
                           'selecttype' =>  'one', 'itemid'  => -1)
  last_post_id = events['events'][0]['itemid']
  $log.debug "Most recent post is #{last_post_id}"
  post_ids = []
  get_args = {
    'selecttype' => 'syncitems'
  }
  progressbar = ProgressBar.new("Working", last_post_id.to_i)
  total_posts = 0
  changed_posts = 0
  loop do
    $log.debug "Asking server for posts"
    events = lj.getevents(get_args)
    start_time = events['events'].map{|e| e['eventtime']}.min
    sync_time = events['events'].map{|e| e['eventtime']}.max
    $log.debug "Received eventds from #{start_time} to #{sync_time}"
    get_args['lastsync'] = sync_time
    events['events'].each do |event|
      total_posts += 1
      if ((not unlock) and is_public?(event)) or 
          (unlock and is_friends_only?(event)) then
        edit_args = {}
        ['itemid', 'event', 'subject', 'props'].each{|key| 
          edit_args[key] = event[key]
        }
        if unlock then
          edit_args['security'] = 'public'
        else
          edit_args['security'] = 'usemask'
          edit_args['allowmask'] = 0x01 # bit zero is the "friends" bit
        end
        unless test_mode
          $log.debug "Setting security preferences for post #{event['itemid']}"
          lj.editevent(edit_args)
          changed_posts += 1
        end
      end
      progressbar.set(event['itemid'].to_i)
    end
    break if events['events'].detect{|e| e['itemid'] == last_post_id}
  end
  progressbar.finish

  $log.info "Finished. #{changed_posts} of #{total_posts} modified."
  
rescue Interrupt
  puts "Process interrupted by user."
rescue XMLRPC::FaultException => error
  if error.faultCode.to_i == 406 then
    $log.fatal "Livejournal is being a butt.  Try again later."
  else
    $log.fatal "XMLRPC error #{error.faultCode}: #{error.faultString}" 
  end
end

View All

15 Comments

  1. Oh hell yeah, I love to see scripted tools for modifying stuff like that. (BTW that URL gives me a 500 error when I wget it)

    How do you like Ruby? I’ve never touched it but I have heard good things. I’ll take a peek at your code sometime.

    1. Yipe, it’s pretty quick-and-dirty code. Don’t hold it up as an example of good Ruby code.

      I’m working on fixing the server error. I hate my web host with a passion.

      I love Ruby. It’s by far the most productive language I’ve ever worked with, and I’ve worked with a lot. It has all the conceptual elegance of Smalltalk combined with all the expressiveness and raw power of Perl, and it has a terrific user community.

    2. I updated the entry with the source code, since I couldn’t get my host to behave.

  2. You know what they say about laguages that use whitespace for syntax don’t you? 😀

    1. Better than the languages that have a whitespace operator 😉

      Ruby doesn’t use whitespace for syntax though. You’re thinking of Python.

      Well, OK, technicaly it pays some attention to whitespace It has some do-what-I-mean heuristics that enable the coder to leave out semicolons at the end of lines. But nothing like Python’s significant indentation.

  3. unless i misunderstand the issue, all you have to do is edit the existing post and select “friends-only”

    1. This is a batch locker. It locks down every public entry you’ve ever made. The only other way to do this, according to the LJ FAQ, is to manually go through every entry one-by-one.

  4. in addition to giving me a great tool to finally get around to locking down my journal, you’ve also given me a straight-forward example of such code that i’m looking forward to learning from. (quick-n-dirty, i know, but that’s how i code when i code ;p)
    thanks much, man!

    1. You know, I don’t know you, but with an icon like that (Computer Liberation and Eva), you’ve GOT to be cool. 🙂

  5. There *is* a version of Semagic which will do the same thing, but it’s one of the older versions and hard to find if you don’t know exactly which version you’re looking for.

    1. I told him you knew about one that would do what he was looking for. However, when he looked he could not find. And he was up for a fun project, so…

  6. Hi, got pointed here by a friend who was interested in using your code. Just had a couple of questions.

    First, shouldn’t it be #!/usr/bin/env ruby?

    Second, I was just curious, why choose usage for parsing command line options? Simplicity? I was looking for a nice library to handle that just a couple days ago, but didn’t come across usage in my search.

    1. First, shouldn’t it be #!/usr/bin/env ruby?

      LJ ate the first two characters. Don’t ask me why.

      why choose usage for parsing command line options?

      OptionParser and it’s progeny are great, but for quick scripts it’s overkill. For the latter, Usage is brilliantly succinct. Just give it the app’s usage as you would for a manpage, and it parses the command line for you, complains to the user if they call it with unknown options, and exposes the resulting values as members. It’s packaged as a gem too, so it’s easy to get.

      By the way, I wouldn’t recommend using this code as-is. Apparently the method it uses for downloading posts is frowned upon by LJ. At some point I hope to release a version which is more kosher.

      1. Usage does look quite nice. I’ll have to give it a shot sometime, though I went with Getopt::Long for the script I was previously working on. Having a parse that complains to the user for you is definitely a plus.

        I don’t plan on locking my entries, though the aforementioned friend did want to use your script to do so. How are you supposed to download all the posts from LJ, if not the way you’re doing it? (Also, thanks for introducing me to these nifty gems. They’re quite interesting.)

        Oh, and there is Linux support for the Airport Express, by the way. One of my friends is using Kubuntu on his PowerBook G4, though it was a little bit of work.

        1. Definitely look at some of the other parsers available if you’ve only worked with Getopt::Long. Ruby 1.8 includes OptionParser, and there are some nice derivatives of it like CommandParser available as gems.

          I haven’t done a great deal of research, and the documentation is awful. But apparently, despite providing a “lastsync” parameter in the getitems call, LJ considers folks who use it to download all of their posts to be abusive. Which strikes me as dumb, because the only alternative that I can see is to use syncitems to get a list of post IDs, and then individually call getitem for each post. This would seem to be a far less efficient use of server resources, but what do I know.

          [Another paragraph ranting about the stupidity of the LJ API elided.]

Comments are closed.