# ----------------------------------------------------------------------------
#
# Charm: A Python-based client for LiveJournal.
#
# Version 1.5.0
# November 9th, 2004
# Copyright (C) 2001, 2002, 2003, 2004  Lydia Leong (evilhat@livejournal.com)
# GNU Public License
#
# Usage: charm --help
#
# (Don't invoke this module directly. It's meant to be imported.)
#
# Written for Python 2.3, but should work with other versions as well.
#
# ----------------------------------------------------------------------------

import sys
import os
import os.path
import string
import getopt
import time
import stat
import md5
import urllib
import calendar

try:
    import xmms.control
except:
    pass

# ----------------------------------------------------------------------------
# Constants.
# ----------------------------------------------------------------------------

__version__ = "1.5.0"
__doc__ = "Charm, a Python-based client for LiveJournal."
__author__ = "evilhat@livejournal.com"

Client_Name = "python-Charm"

barline = "------------------------------------------------------------------------------"

# A '1' indicates that the option's normal value is reversed.

Bool_Opts = { "archive" : 0, "archive_edits" : 0, "archive_overwrite" : 0,
	      "archive_subdirs" : 0, "show_permissions" : 0, "nocache" : 0,
	      "nologin" : 0, "autoformat" : 1, "comments" : 1,
	      "noemail" : 0, "backdate" : 0, "edit_times" : 0,
              "autodetect" : 0 }

Bool_Map = { "archive" : "archive",
	     "archive_edits" : "archive_edits",
	     "archive_overwrite" : "archive_overwrite",
	     "archive_subdirs" : "archive_subdirs",
	     "show_permissions" : "showperms",
	     "nocache" : "nocache", "nologin" : "nologin",
	     "autoformat" : "prop_opt_preformatted",
	     "comments" : "prop_opt_nocomments",
	     "noemail" : "prop_opt_noemail",
	     "backdate" : "prop_opt_backdated",
	     "edit_times" : "edit_times",
             "autodetect" : "autodetect" }

Other_Opts = { "archive_dir" : 1, "draft_dir" : 1, "organize" : 1,
	       "editor" : 0, "pager" : 0, "spellchecker" : 0,
	       "user" : 1, "password" : 1, "hpassword" : 1,
	       "login" : 1, "hlogin" : 1, "default_user" : 0,
	       "groupheader" : 1, "commpic" : 1,
	       "security" : 1, "journal" : 1, "url" : 0,
	       "checkgroups" : 1, "checkdelay" : 1 }

Basic_MetaData = [ "year", "mon", "day", "hour", "min",
		   "subject", "usejournal", "poster",
		   "security", "allowmask",
		   "prop_current_mood", "prop_current_moodid",
		   "prop_picture_keyword", "prop_current_music",
		   "prop_opt_preformatted",
		   "prop_opt_backdated",
		   "prop_opt_nocomments", "prop_opt_noemail" ]

Conf_MetaData = [ "prop_opt_preformatted", "prop_opt_nocomments",
		  "prop_opt_noemail", "prop_opt_backdated",
		  "usemask", "allowmask" ]

# ----------------------------------------------------------------------------
# Miscellaneous functions.
# ----------------------------------------------------------------------------

def client_info():
    "Print client information."

    print barline
    print
    print "Client ID: %s/%s" % (Client_Name, __version__)
    print """
This LiveJournal client was written by evilhat.
Copyright 2001, 2002, 2003, 2004. Terms of the GNU Public License apply.
The author can be reached at evilhat@livejournal.com"""
    print
    print barline


def sane_raw_input(prompt = "Action: "):
    "Handles EOF more gracefully."

    try:
	res = raw_input(prompt)
    except EOFError:
	res = ""
    return res


def dump_dict(this_dict):
    "Dump contents of a dictionary."

    for k in this_dict.keys():
	print "%s = %s" % (k, this_dict[k])


def parse_bool(instr):
    "Parse a boolean string, return 0 or 1, or -1 on error."

    if instr in ("yes", "y", "true", "t", "on", "1"):
	return 1
    elif instr in ("no", "n", "false", "f", "off", "0"):
	return 0
    else:
	return -1


def create_dir(dname, derr):
    "Create a directory if it doesn't exist."

    if not os.path.exists(dname):
	try:
	    os.mkdir(dname)
	except OSError:
	    print "Error creating %s directory %s" % (derr, directory)
	    return 0
	try:
	    os.chmod(dname, 0700)
	except OSError:
	    pass			# just ignore
    return 1


def column_table(list, n_cols):
    "Print a list, numbered, in a certain number of columns."

    l_len = len(list)
    width = (78 / n_cols) - 3
    if l_len > 99:
	width = width - 2
    if l_len > 9:
	width = width - 1

    i = 1
    for k in list:
	if i % n_cols == 0:		# last item in column
	    print "%-4s %s" % (str(i) + ".", k[:width])
	else:
	    print "%-4s %s " % (str(i) + ".",
			      string.ljust(k[:width], width)),
	i = i + 1
    if i % n_cols != 1:			# just went past last item
	print


def md5digest(sstr):
    "md5digest hex digestify a string."

    # -- Newer versions of python have hexdigest() built into the md5
    #    module. We provide an alternative, for earlier versions.

    try:
	hexd = md5.hexdigest(sstr)
    except AttributeError:
	digest = md5.new(sstr).digest()
	hexd = ""
	for c in digest:
	    hexd = hexd + string.hexdigits[ord(c) / 16]
	    hexd = hexd + string.hexdigits[ord(c) % 16]

    return hexd


def commalist(list):
    "Create a comma-separated list string, out of a list of strings."

    llen = len(list)
    if llen == 0:
	return ""
    elif llen == 1:
	return list[0]
    else:
	outstr = list[0]
	for w in list[1:]:
	    outstr = outstr + ", " + w
	return outstr


def truncstr_more(str, len, more_str = " [...more]"):
    "Truncate a string to a specified length."

    return string.replace(string.replace(str[:len], "\r", " "),
			  "\n", " ") + more_str


def append_htblock(orig, new):
    "Append a paragraph to an existing HTML text body. Return string."

    if orig[-2:] == "\n\n":
	tstr = orig
	orig = orig[:-2]
    elif orig[-1] == "\n":
	tstr = orig + "\n"
	orig = orig[:-1]
    else:
	tstr = orig + "\n\n"

    if orig[-3:] == "<P>" or orig[-3:] == "<p>":
	return tstr + new + "\n<P>\n"
    elif orig[-4:] == "</P>" or orig[-4:] == "</p>":
	return tstr + "<P>" + new + "\n</P>\n"
    else:
	return tstr + "<P>\n\n" + new + "\n"


def valid_hexcolor(s):
    "Is a string a valid hex color?"

    if s == "":
        return 0

    if s[0] != "#":
        return 0

    for c in s[1:]:
        if c not in ("A", "a", "B", "b", "C", "c", "D", "d", "E", "e",
                     "F", "f", "0", "1", "2", "3", "4", "5", "6", "7",
                     "8", "9"):
            return 0

    return 1    

# ----------------------------------------------------------------------------
# Dealing with Unicode.
# ----------------------------------------------------------------------------

def utf8(s):
    "UTF-8 encode a string, if supported."

    try:
        return s.encode("UTF-8")
    except:
        return s


def utf8_urlencode(data):
    "Equivalent of urllib.urlencode(), but handles Unicode data."

    out = []

    for (k, v) in data.items():

        # -- x-www-form-urlencoded requires normalization of newlines to \r\n
        #    Lack of list comprehensions in older versions prevent us from
        #    just writing:
        #    v = "\r\n".join( [ x.replace("\n", "\r\n")
        #                       for x in v.split("\r\n") ] )

        elems = []
        for x in string.split(v, "\r\n"):
            elems.append(string.replace(x, "\n", "\r\n"))
        v = string.join(elems, "\r\n")

        # -- Split on spaces and rejoin with pluses, and recreate each
        #    segment by leaving unreserved characters alone and UTF-8 and
        #    percent-encoding the others.
        #    Concise expression:
        #    v = '+'.join( [ ''.join( [ octet in unreserved and octet or
        #                               u'%%%02X' % ord(octet) for octet
        #                               in word.encode('utf-8')] )
        #                       for word in v.split(' ') ] )

        words = string.split(v, " ")
        enc = []
        for w in words:
            res = []
            for c in w.encode("UTF-8"):
                if c not in urllib.always_safe:
                    c = u'%%%02X' % ord(c)
                res.append(c)
            enc.append(string.join(res, ""))
        v = string.join(enc, "+")

        out.append("%s=%s" % (k, v.encode("us-ascii")))

    return string.join(out, "&")
   

# ----------------------------------------------------------------------------
# Override the URL opener object so we can set cookies.
# ----------------------------------------------------------------------------

class CustomURLopener(urllib.FancyURLopener):
    def __init__(self, *args):
	apply(urllib.FancyURLopener.__init__, (self,) + args)


# ----------------------------------------------------------------------------
# Object to cache data returned from server across invocations of client.
# ----------------------------------------------------------------------------

class LJ_Cache:
    "Server data cache."

    def __init__(self):
	"Initialize."

	self.Moods = {}
	self.Mood_Count = 0
	self.Journals = []
	self.PicKws = []
	self.Friends = {}
	self.FriendSorter = {}
	self.Irrelevant = []

    def load_cache(self, fname, username):
	"Load cache file."

	try:
	    f = open(fname, 'r')
	except IOError:
	    return			# just ignore
	line = f.readline()
	while line != "":
	    line = line[:-1]		# zap newline
	    if line == "":
		pass
	    else:
		inpair = string.split(line, '=', 1)
		ustr = "%s " % (username)
		ulen = len(ustr)
		if inpair[0][:5] == "mood_":
		    self.Moods[inpair[0][5:]] = inpair[1]
		elif inpair[0][:ulen] == ustr:
		    # -- Read only the data associated with our username.
		    if inpair[0][ulen:] == "pic":
			self.PicKws.append(inpair[1])
		    elif inpair[0][ulen:] == "journal":
			self.Journals.append(inpair[1])
		    elif inpair[0][ulen:][:3] == "fg_":
			self.Friends[inpair[0][ulen:][3:]] = inpair[1]
		    elif inpair[0][ulen:][:4] == "ofg_":
			self.FriendSorter[inpair[0][ulen:][4:]] = inpair[1]
		else:
		    # -- Must still save data from other usernames.
		    self.Irrelevant.append(line)
	    line = f.readline()
	f.close()

	self.Mood_Count = 0
	for k in self.Moods.keys():
	    n = int(self.Moods[k])
	    if n > self.Mood_Count:
		self.Mood_Count = n 


    def save_cache(self, username):
	"Save cache file."

	if self.Mood_Count < 1:
	    return

	try:
	    user_home_dir = os.environ['HOME']
	except KeyError:
	    user_home_dir = "/tmp"
	fname = "%s/.charmcache" % (user_home_dir)
	cfile = open(fname, 'w')
	for k in self.Moods.keys():
	    cfile.write("mood_%s=%s\n" % (k, self.Moods[k]))
	for k in self.PicKws:
	    cfile.write("%s pic=%s\n" % (username, k))
	for k in self.Journals:
	    cfile.write("%s journal=%s\n" % (username, k))
	for k in self.Friends.keys():
	    cfile.write("%s fg_%s=%s\n" % (username, k, self.Friends[k]))
	for k in self.FriendSorter.keys():
	    cfile.write("%s ofg_%s=%s\n" % (username, k, self.FriendSorter[k]))
	for k in self.Irrelevant:
	    cfile.write("%s\n" % (k))
	cfile.close()


    def load_moods_from_net(self, net_dict):
	"Given a dictionary containing network output, load moods."

	# -- Indexed by name. The data contents are the IDs.

	try:
	    max_moods = int(net_dict["mood_count"])
	    if max_moods != 0:
		for n in range(1, max_moods + 1):
		    try:
			idstr = net_dict["mood_%d_id" % (n)]
			self.Moods[net_dict["mood_%d_name" % (n)]] = idstr
			idn = int(idstr)
			if idn > self.Mood_Count:
			    self.Mood_Count = idn
		    except KeyError:
			pass
	except KeyError:
	    pass


    def load_journals_from_net(self, net_dict):
	"Given a dictionary containing network output, load journals."

	# -- Indexed by NAME, not number. 

	self.Journals = []

	try:
	    max_journs = int(net_dict["access_count"])
	    if max_journs != 0:
		for n in range(1, max_journs + 1):
		    try:
			self.Journals.append(net_dict["access_%d" % (n)])
		    except KeyError:
			pass
	except KeyError:
	    pass


    def load_pickws_from_net(self, net_dict):
	"Given a dictionary containing network output, load picture keywords."

	self.PicKws = []

	try:
	    max_pk = int(net_dict["pickw_count"])
	    if max_pk != 0:
		for n in range(1, max_pk + 1):
		    try:
			self.PicKws.append(net_dict["pickw_%d" % (n)])
		    except KeyError:
			pass
	except KeyError:
	    pass


    def load_frgrps_from_net(self, net_dict):
	"Given a dictionary containing network output, load friend groups."

	# -- Process friend groups. Indexed by name. Store bitmask (the
	#    friend group number corresponds to the bit to turn on).

	self.Friends = {}
	self.FriendSorter = {}

	try:
	    max_frgrps = int(net_dict["frgrp_maxnum"])
	    if max_frgrps != 0:
		for n in range(1, max_frgrps + 1):
		    try:
			frgrpname = net_dict["frgrp_%d_name" % (n)]
			self.Friends[frgrpname] = str(pow(2, n))
			try:
			    sortord = int(net_dict["frgrp_%d_sortorder" % (n)])
			except KeyError:
			    sortord = 50
			self.FriendSorter[frgrpname] = sortord
		    except KeyError:
			pass
	except KeyError:
	    pass


# ----------------------------------------------------------------------------
# The do-it-all object.
# ----------------------------------------------------------------------------

class Jabber:
    "The do-everything object." 

    def __init__(self):
	"Set initial configuration parameters."

	self.FastServer = 0

	self.Params = { "clientversion" : Client_Name + "/" + __version__,
			"url" : "http://www.livejournal.com/interface/flat",
			"archive" : "1", "organize" : "month",
			"archive_edits" : "1", "archive_overwrite" : "0",
			"archive_subdirs" : "0", "showperms" : "0",
			"getmoods" : "0", "getpickws" : "1" }

	self.Save_Meta = { }

	# -- Figure out what type of line endings we (hopefully) have,
	#    based on our system type.

	if os.name == "posix":
	    le = "unix"
	elif os.name in ("nt", "dos", "os2", "ce"):
	    le = "pc"
	elif os.name == "mac":
	    le = "mac"
	else:
	    le = "unix"			# something mysterious. pray.
	self.Params["lineendings"] = le

	# -- Look for our important directories.

	try:
	    user_home_dir = os.environ['HOME']
	except KeyError:
	    user_home_dir = "/tmp"
	self.Params["draft_dir"] = "%s/.ljdrafts" % (user_home_dir) 
	self.Params["archive_dir"] = "%s/.ljarchive" % (user_home_dir)

	# -- Find what editor we'd like to use.

	try:
	    self.Params["editor"] = os.environ['VISUAL']
	except KeyError:
	    try:
		self.Params["editor"] = os.environ['EDITOR']
	    except KeyError:
		self.Params["editor"] = "vi"

	# -- Initialize miscellaneous other stuff.

	self.Cache = LJ_Cache()

	self.NonRecurs = []
	self.Logins = {}

	self.Sent = ""
	self.Got = {}
	self.LoggedIn = 0
	self.GottenFriends = 0

	self.Mood = ""

	self.Entries = []
	self.Entry = {}
	self.GroupHeaders = {}
	self.CommPics = {}

	self.CheckDelay = 0
	self.CheckGroups = []

    # -----
    # Helpful routines.
    # -----

    def getval(self, pname, dstr = "(none)" ):
	"Return parameter with name pname, or dstr if it doesn't exist."

	if self.Params.has_key(pname):
	    return self.Params[pname]
	else:
	    return dstr


    def del_param(self, pname):
	"Delete parameter, if it exists."

	try:
	    del self.Params[pname]
	except KeyError:
	    pass


    def dump_debug_info(self):
	"Print the debugging info we need most often."

	print
	print "--------------------------[ Dumping Debugging Info ]--------------------------"
	print
	print "Params:"
	dump_dict(self.Params)
	print
	print "Journals:"
	for k in self.Cache.Journals:
	    print k
	print
	print "Friends:"
	dump_dict(self.Cache.Friends)
	print
	print "Picture keywords:"
	for k in self.Cache.PicKws:
	    print k
	print
#	print "Moods:"
#	dump_dict(self.Cache.Moods)
#	print
	print "Sent:"
	print self.Sent
	print
	print "Got:"
	dump_dict(self.Got)
	print barline
	print


    def format_time(self, d = None):
	"Return the current post time as a string."

	if d is None:
	    d = self.Params

	return "%s-%s-%s %s:%s" % (d["year"], d["mon"], d["day"],
				   d["hour"], d["min"])


    def populate_time(self, timetuple):
	"Populate the time parameters with a time."

	self.Params["year"] = time.strftime("%Y", timetuple)
	self.Params["mon"] = time.strftime("%m", timetuple)
	self.Params["day"] = time.strftime("%d", timetuple)
	self.Params["hour"] = time.strftime("%H", timetuple)
	self.Params["min"] = time.strftime("%M", timetuple)


    def copy_net_data(self, pstr, klist):
	"Copy from network data, with a prefix string."

	for k in klist:
	    try:
		self.Params[k] = self.Got[pstr + k]
	    except KeyError:
		pass


    def set_special_opt(self, pname, pval):
	"Set a special option that requires pre-processing."

	if pname == "user":
	    # -- Ignore this, because the user is going to get complained
	    #    at when we get to the password option, and we shouldn't
	    #    do that twice.
	    pass

	elif pname in ("password", "hpassword"):
	    print """\
The options user, password, and hpassword are deprecated. Please use:
    login = username password
or
    hlogin = username MD5hexpassword
in your .charmrc file instead."""

	elif pname in ("draft_dir", "archive_dir"):
	    self.Params[pname] = os.path.expanduser(pval)

	elif pname == "organize":
	    if pval in ("none", "year", "month"):
		self.Params[pname] = pval
	    else:
		print "Warning: invalid value for option " + pname

	elif pname == "login":
	    inpair = string.split(pval, ' ', 1)
	    try:
		self.Logins[inpair[0]] = md5digest(inpair[1])
	    except IndexError:
		print "Warning: malformed value for login option."

	elif pname == "hlogin":
	    inpair = string.split(pval, ' ', 1)
	    try:
		self.Logins[inpair[0]] = inpair[1]
	    except IndexError:
		print "Warning: malformed value for hlogin option."

	elif pname == "groupheader":
	    inpair = string.split(pval, ',', 1)
	    try:
		self.GroupHeaders[inpair[0]] = inpair[1]
	    except IndexError:
		print "Warning: malformed value for groupheader option."

	elif pname == "commpic":
	    inpair = string.split(pval, ',', 1)
	    try:
		self.CommPics[inpair[0]] = inpair[1]
	    except IndexError:
		print "Warning: malformed value for commpic option."

	elif pname == "security":
	    # -- We have to do this later, after we log in, so we can
	    #    get group names.
	    if pval != "":
		self.Params["security"] = pval

	elif pname == "journal":
	    # -- No error-checking.
	    self.Params["usejournal"] = string.lower(pval)

	elif pname == "checkgroups":
	    self.CheckGroups = map(lambda x: string.lower(x),
				   string.split(pval, ','))

	elif pname == "checkdelay":
	    try:
		n = int(pval) * 60
		if n <= 0:
		    print "Warning: bad value for checkdelay option, ignoring."
		else:
		    self.CheckDelay = n
	    except ValueError:
		print "Warning: malformed value for checkdelay option."

	else:
	    # Huh. Unimplemented. Do default.
	    self.Params[pname] = pval


    def read_rcfile(self, rcfile=".charmrc"):
	"Read in config info from an rc file of key=value pairs."

	if rcfile in self.NonRecurs:
	    print "Attempt to recursively read rcfile " + rcfile + ", skipped."
	    return
	self.NonRecurs.append(rcfile)

	try:
	    f = open(rcfile, 'r')
	except IOError:
	    if self.NonRecurs == [ rcfile ]:
		print "Unable to open rcfile " + rcfile + ", exiting."
		sys.exit(1)
	    else:
		print "Unable to open rcfile " + rcfile + ", skipped."
		return

	line = f.readline()
	while line != "":
	    line = line[:-1]		# discard newline
	    if line == "":
		pass
	    elif line[0] == "#":	# comment line, discard
		pass
	    elif line[:8] == "include ":
		self.read_rcfile(os.path.expanduser(line[8:]))
	    else:
		inpair = string.split(line, '=', 1)
		if len(inpair) < 2:
		    print "Warning: invalid line, " + line
		else:
		    pname = string.lower(string.strip(inpair[0]))
		    pval = string.strip(inpair[1])
		    try:
			# -- Here is our song and dance to deal with boolean
			#    options, since we want nice short option names for
			#    users, rather than the big long non-intuitive
			#    names used by the protocol. Map the names, check
			#    for reversal, and, if we now have a 1, set it.

			preal = Bool_Map[pname]
			pnum = parse_bool(pval)
			if pnum == -1:
			    print "Warning: invalid value for boolean option %s" % (pname)
			else:
			    if Bool_Opts[pname] == 1:
				if pnum == 0:
				    pnum = 1
				else:
				    pnum = 0
			    if pnum == 1:
				self.Params[preal] = str(pnum)
			    else:
				if self.Params.has_key(preal):
				    del self.Params[preal]
		    except KeyError:
			try:
			    if Other_Opts[pname] == 0:
				self.Params[pname] = pval
			    else:
				self.set_special_opt(pname, pval)
			except KeyError:
			    print "Warning: invalid option name " + pname
	    line = f.readline()
	f.close()


    def autodetect_music(self):
        "Set and return the title of auto-detected music, if we can manage it."

        music = ""
        try:
            if self.Params.has_key("autodetect") and xmms.control.is_running(0) and xmms.control.is_playing(0):
                music = string.strip(xmms.control.get_playlist_title(xmms.control.get_playlist_pos(0), 0))
        except:
            pass

        if music != "":
            self.Params["prop_current_music"] = music
            self.Params["autodetect"] = 1
            return music
        else:
            return "(none)"


    def sort_friendgroups(self):
	"Return friend group list in sorted order, by sort priority."

	# -- Python versions before 2.0 don't have list comprehensions,
	#    or we'd replace the maps/lambdas with:
	# items = [ (v, k) for k, v in self.Cache.FriendSorter.items() ]
	# olist = [ k for v, k in items ]

	items = map(lambda x: (x[1], x[0]), self.Cache.FriendSorter.items())
        items.sort()
	olist = map(lambda x: x[1], items)
	return olist


    def make_draft_file(self, timestr):
	"Set up for a draft file."

	# -- If we don't have a drafts directory, make one.

	ddir = self.Params["draft_dir"]
	ok = create_dir(ddir, "drafts")
	if ok == 0:
	    return 0

	# -- Drafts are named after timestamps.

	dfile = "%s/draft_%s" % (ddir, timestr)
	self.Params["draft_file"] = dfile
	return 1


    # -----
    # Draft file and meta-data utilities.
    # -----

    def clear_metadata(self):
	"Clear out metadata."

	for k in Basic_MetaData:
	    try:
		del self.Params[k]
	    except KeyError:
		pass
	self.Mood = ""


    def reset_metadata(self):
	"Restore metadata to defaults."

	self.clear_metadata()
	for k in Conf_MetaData:
	    try:
		self.Params[k] = self.Save_Meta[k]
	    except KeyError:
		pass


    def save_metadata(self, timestr):
	"Save metadata."

	# -- Our base directory is the base directory of our drafts file,
	#    if we have one. Otherwise it's our drafts directory.

	try:
	    mfname = "%s/.meta_%s" % \
		     (os.path.dirname(self.Params["draft_file"]), timestr)
	except KeyError:
	    mfname = "%s/.meta_%s" % (self.Params["draft_dir"], timestr)

	try:
	    mfile = open(mfname, 'w')
	    for k in Basic_MetaData:
		try:
		    mfile.write("%s=%s\n" % (k, self.Params[k]))
		except KeyError:
		    pass
	    mfile.close()
	except IOError:
	    pass			# whoops. oh well.


    def save_session(self, noisy = 0):
	"Save a session."

	# -- Important note. If we're operating in noisy mode, and
	#    the user chooses not to save, wipe out the draft file.

	try:
	    dfile = self.Params["draft_file"]
	    if dfile != "":
		dbase = os.path.basename(dfile)
		ddir = os.path.dirname(dfile)
		timestr = string.join((string.split(dbase, '_'))[-2:], '_')

		if noisy == 1:
                    res = sane_raw_input("Do you want to save the current session (Y/N)? ")
		    print
		    res = string.lower(string.strip(res))

		    # -- Err on the side of caution. If we get malformed
		    #    input we'd rather save than not.

		    if res in ("n", "no"):
			mfname = "%s/.meta_%s" % (ddir, timestr)
			try:
			    os.unlink(dfile)
			except OSError:
			    pass
			os.unlink(mfname)
			return

		    print "Saving current session. You can resume it with:\n\t%s -r" % (sys.argv[0]),
		    if ddir == self.Params["draft_dir"]:
			print dbase
		    else:
			print dfile

		self.save_metadata(timestr)
		print
	except KeyError:
	    pass


    def clear_session(self, dfile = ""):
	"Delete old files and clear out meta-data."

	if dfile == "":
	    dfile = self.Params["draft_file"]

	dbase = os.path.basename(dfile)
	ddir = os.path.dirname(dfile)
	timestr = string.join((string.split(dbase, '_'))[-2:], '_')
	mfname = "%s/.meta_%s" % (ddir, timestr)
	try:
	    os.unlink(dfile)
	except OSError:
	    pass
	os.unlink(mfname)
	self.reset_metadata()

	for k in [ "draft_file", "event", "itemid" ]:
	    try:
		del self.Params[k]
	    except KeyError:
		pass


    def read_event(self, f):
	"Read a file into the event parameter."

	body = []
	line = f.readline()
	while line != "":
	    body.append(line)
	    line = f.readline()
	self.Params["event"] = string.join(body, "")


    def slurp_draft(self, optype):
	"Read a draft file into the event parameter."

	# -- First, we need a non-empty draft file that we can read.

	try:
	    dfile = self.Params["draft_file"]
	except KeyError:
	    print optype + " attempt cancelled: No current draft."
	    return 0

	if os.access(dfile, os.F_OK | os.R_OK) == 0:
	    print "%s attempt cancelled: Cannot read the current draft file." % (optype)
	    return 0

	if (os.stat(dfile))[stat.ST_SIZE] == 0:
	    print optype + " attempt cancelled: Draft file is empty."
	    return 0

	try:
	    f = open(dfile, 'r')
	except IOError:
	    print optype + " attempt cancelled: Error reading draft file."
	    return 0

	# -- Read the draft file into a really big buffer.

	self.read_event(f)
	f.close()
	return 1


    # -----
    # Web operation handlers.
    # -----

    def web_encode(self, mode, has_utf, klist, blist = []):
	"Encode a list of parameters into a URL string."

        pdict = { "mode": mode, "ver" : utf8(str(has_utf)) }

	for k in klist:
	    try:
		if self.Params[k] != "":
		    pdict[k] = utf8(self.Params[k])
	    except KeyError:
		pass			# if we don't have it, just ignore it

	# -- For these keys, force encoding even if blank.

	for k in blist:
	    try:
		pdict[k] = utf8(self.Params[k])
	    except KeyError:
		pdict[k] = ""

        if has_utf:
            s = utf8_urlencode(pdict)
        else:
            s = urllib.urlencode(pdict)
            
	return s


    def parse_return(self, optype):
	"Parse server-returned success code."

	try:
	    succ_code = self.Got["success"]
	    if succ_code == "FAIL":
		try:
		    self.show(optype + " failed: " + self.Got["errmsg"])
		except KeyError:
		    self.show(optype + " failed, no error message specified.")
		return 0
	except KeyError:
	    self.show(optype + " failed, server error. Try again later.")
	    return 0
	return 1


    def raw_client_op(self):
	"Client operation: Send request, read response."

	# -- urllib doesn't play nice with cookies. Kludge for fast server.

	tmp = CustomURLopener()
	if self.FastServer == 1:
	    tmp.addheader('Cookie', 'ljfastserver=1')
	urllib._urlopener = tmp

	# - Get it, parse it, save it.

	try:
	    netobj = urllib.urlopen(self.Params["url"], self.Sent)
	except:
	    print "\nNetwork error. Try again later.\n"
	    self.Got = {}
	    return

	self.Got = {}
	str = ""
	line = netobj.readline()
	while line != "":
	    line = line[:-1]		# discard newline
	    if str == "":
		str = line
	    else:
		self.Got[str] = line
		str = ""
	    line = netobj.readline()


    def client_op(self, mode, klist, blist = []):
        """Client operation: Obtain a challenge string, if possible. Use
           this to authenticate the full operation, sending data and
           reading the response."""

        # -- Not all python versions support Unicode. For those that
        #    don't, we use the protocol version 0.

        try:
            a = unicode("a")
            ver = 1
        except:
            ver = 0

        self.Sent = self.web_encode("getchallenge", ver, [ "user" ])
        self.raw_client_op()
        ok = self.parse_return("Challenge authentication")
        if ok == 0:
            # Fall back on auth in the clear.
            self.del_param("auth_method")
            self.del_param("auth_challenge")
            self.del_param("auth_response")
            self.Sent = self.web_encode(mode, ver,
                                        klist + [ "user", "hpassword" ], blist)
        else:
            self.Params["auth_method"] = "challenge"
            self.Params["auth_challenge"] = self.Got["challenge"]
            self.Params["auth_response"] = md5digest("%s%s" % (self.Got["challenge"], self.Params["hpassword"]))
            self.Sent = self.web_encode(mode, ver,
                                        klist + [ "user", "auth_method",
                                                  "auth_challenge",
                                                  "auth_response" ], blist)
        self.raw_client_op()


    def console_command(self, commands):
        "XML-RPC console command."

        try:
            import xmlrpclib
        except ImportError:
            self.Got["success"] = "FAIL"
            self.Got["errmsg"] = "Your Python installation lacks the required xmlrpclib module."
            return

        # -- Fix up the flat URL into an XML-RPC one.

        raw_url_elems = self.Params["url"].split("/")
        url = "%s/xmlrpc" % ("/".join(raw_url_elems[:-1]))

        server = xmlrpclib.Server(url)
        data = {}

        # -- Try challenge/auth first. Fall back to clear if it fails.

        try:
            self.Got = server.LJ.XMLRPC.getchallenge()
            data["auth_method"] = "challenge"
            data["auth_challenge"] = self.Got["challenge"]
            data["auth_response"] = md5digest("%s%s" % (self.Got["challenge"], self.Params["hpassword"]))
        except xmlrpclib.Error, faultobj:
            data["hpassword"] = self.Params["hpassword"]

        data["username"] = self.Params["user"]
        data["ver"] = "1"
        data["commands"] = commands

        # -- Send the commands.
        
        try:
            self.Got = server.LJ.XMLRPC.consolecommand(data)
            self.Got["success"] = "OK"
        except xmlrpclib.Error, faultobj:
            self.Got["errmsg"] = faultobj.faultString
            self.Got["success"] = "FAIL"


    def show(self, msg):
	"Show a word-wrapped message in an output window."

	out = ""
	lines = string.split(msg, '\n')
	for text in lines:
	    cur_len = 0
	    words = string.split(str(text), ' ')
	    for w in words:
		this_word = str(w)
		this_len = len(this_word)
		if cur_len + this_len > 75:
		    if cur_len == 0:	# print single word even if too long
			out = out + this_word
		    else:
			print out
			out = this_word
		    cur_len = this_len
		else:
		    if cur_len != 0:
			out = out + " " + this_word
			cur_len = cur_len + this_len + 1
		    else:
			out = out + this_word
			cur_len = this_len
	    print out
	    out = ""


    def process_getfriendgroups(self):
	"Process returned friend groups."

	self.Cache.load_frgrps_from_net(self.Got)
	self.GottenFriends = 1


    def cli_checkfriends(self, silent = 0):
	"Client request: checkfriends"

	self.client_op("checkfriends", [ "lastupdate", "mask" ])

	ok = self.parse_return("Friends check")
	if ok == 0:
	    return

	try:
	    self.Params["lastupdate"] = self.Got["lastupdate"]
	except KeyError:
	    pass			# hmm, that's not good

	if silent == 0:
	    try:
		if self.Got["new"] == "1":
		    print "You have new friend updates."
		else:
		    print "No new friend updates."
	    except KeyError:
		print "Server error. Try again later."


    def cli_login(self, no_show_info = 0):
	"Client request: login"

	if self.Cache.Mood_Count > 0:
	    self.Params["getmoods"] = str(self.Cache.Mood_Count)

	self.client_op("login", [ "clientversion", "getpickws", "getmoods" ])

	# -- Look for success/failure codes.

	ok = self.parse_return("Login")
	if ok == 0:
	    return

	# -- Welcome the user by name, and, if it exists, print announcement.

	if no_show_info == 0:
	    self.show("Welcome, " + self.Got["name"])
	    try:
		msg = self.Got["message"]
		print
		self.show(msg)
	    except KeyError:
		pass

	# -- Detect if we're allowed to use the fast servers.

	try:
	    fs_code = self.Got["fastserver"]
	    if fs_code == 1:
		self.FastServer = 1
	except KeyError:
	    pass

	# -- Process journals we can post to.

	self.Cache.load_journals_from_net(self.Got)

	# -- Process friend groups.

	self.process_getfriendgroups()

	# -- Process moods.

	self.Cache.load_moods_from_net(self.Got)

	# -- Process picture keywords.

	self.Cache.load_pickws_from_net(self.Got)

	# -- Note that we logged in successfully.

	self.LoggedIn = 1


    def cli_getdaycounts(self):
	"Client request: getdaycounts"

	self.client_op("getdaycounts", [ "usejournal" ])
	return self.parse_return("Retrieval")


    def cli_postevent(self):
	"Client request: postevent"

	self.client_op("postevent", [ "lineendings", "event" ] +
		       Basic_MetaData)
	return self.parse_return("Post")


    def cli_getevents_list(self, tmp_keys):
	"Client request: getevents (download a list)"
	
	self.Params["truncate"] = "55"
	self.Params["prefersubject"] = "1"
	self.Params["noprops"] = "1"

	print "Retrieving..."
	print

	self.client_op("getevents", [ "lineendings",
				      "truncate", "prefersubject", "noprops",
				      "selecttype", "usejournal" ] + tmp_keys)
	
	# -- Clear out our temporary properties.

	for k in [ "truncate", "prefersubject", "noprops",
		   "selecttype" ] + tmp_keys:
	    del self.Params[k]

	# -- Check return code.

	ok = self.parse_return("Retrieval")
	if ok == 0:
	    return 0

	# -- Read in the returned events.

	try:
	    ecount = int(self.Got["events_count"])
	except KeyError:
	    print "No entries returned."
	    return 0
	if ecount < 1:
	    print "No entries returned."
	    return 0

	self.Entries = []
	try:
	    for n in range(1, ecount + 1):
		ljo = {}
		ljo["itemid"] = self.Got["events_%d_itemid" % (n)]
		ljo["time"] = self.Got["events_%s_eventtime" % (n)]
		ljo["subject"] = urllib.unquote_plus(self.Got["events_%d_event" % (n)])
		self.Entries.append(ljo)
	except KeyError:
	    print "Error while processing retrieved entry %d." % (n)

	return 1

    def event_common_data(self, timetuple):
	"Copy common event data."

	self.populate_time(timetuple)

	# -- Take the mood ID if we have it, otherwise use the text.


	if self.Params.has_key("prop_current_moodid"):
	    for k in self.Cache.Moods.keys():
		if self.Cache.Moods[k] == self.Params["prop_current_moodid"]:
		    self.Mood = k
		    break
	else:
	    try:
		self.Mood = self.Params["prop_current_mood"]
	    except KeyError:
		pass


    def cli_getevents_one(self):
	"Retrieve all data about a single event."

	print "Retrieving entry..."
	print

	self.Params["selecttype"] = "one"
	self.Params["itemid"] = self.Entry["itemid"]
	self.client_op("getevents", [ "lineendings",
				      "selecttype", "itemid", "usejournal" ] )
	del self.Params["selecttype"]
	del self.Params["itemid"]

	ok = self.parse_return("Retrieval")
	if ok == 0:
	    return 0

	# -- Read in all our basic data.

	try:
	    if self.Got["events_count"] != "1":
		print "Error. Server returned incorrect entry count: %s" \
		      % (self.Got["events_count"])
		return 0
	except KeyError:
	    print "Error. Server did not return an entry count."
	    return 0

	try:
	    self.Params["itemid"] = self.Got["events_1_itemid"]
	except KeyError:
	    print "Error. Server did not return item ID of entry."
	    return 0

	try:
	    timetuple = time.strptime(self.Got["events_1_eventtime"],
				      "%Y-%m-%d %H:%M:%S")
	except ValueError:
	    try:
		timetuple = time.strptime(self.Got["events_1_eventtime"],
					  "%Y-%m-%d %H:%M")
	    except ValueError:
		print "Server returned malformed entry time. Setting to present."
		timetuple = time.localtime(time.time())

	self.copy_net_data("events_1_", [ "security", "allowmask", "subject" ])

	try:
	    pcstr = self.Got["prop_count"]
	    try:
		pcount = int(pcstr)
		for n in range(1, pcount + 1):
		    self.Params["prop_" + self.Got["prop_%d_name" % (n)]] = self.Got["prop_%d_value" % (n)]
	    except ValueError:
		print "Server returned malformed property count. Ignoring."
	except KeyError:
	    pass

	self.event_common_data(timetuple)

	# -- Now we need to create a draft file.

	timestr = time.strftime("%Y%m%d_%H%M%S", timetuple)
	ok = self.make_draft_file(timestr)
	if ok == 0:
	    return 0			# error message taken care of already

	try:
	    f = open(self.Params["draft_file"], 'w')
	    f.write(urllib.unquote_plus(self.Got["events_1_event"]))
	    f.close()
	except IOError:
	    print "Error writing to draft file."

	self.save_metadata(timestr)
	return 1


    def cli_getevents_day(self, ttup):
	"Retrieve all data about events on a single day."

	self.Params["year"] = time.strftime("%Y", ttup)
	self.Params["month"] = time.strftime("%m", ttup)
	self.Params["day"] = time.strftime("%d", ttup)
	self.Params["selecttype"] = "day"

	self.client_op("getevents", [ "year", "month", "day", "lineendings",
				      "selecttype", "usejournal" ] )
	del self.Params["selecttype"]

	ok = self.parse_return("Retrieval")
	if ok == 0:
	    raise IOError


    def cli_editevent(self):
	"Client request: editevent"

	self.client_op("editevent", [ "lineendings", "itemid", "event" ] +
		       Basic_MetaData )
	return self.parse_return("Edit")


    def cli_delevent(self):
	"Client request: editevent (delete)"

	# -- Send the post with a forcibly blank entry.

	self.client_op("editevent", [ "itemid" ], [ "event" ])
	return self.parse_return("Deletion")


    def cli_getfriendgroups(self):
	"Client request: getfriendgroups"

	self.client_op("getfriendgroups", [])
	ok = self.parse_return("Friend group retrieval")
	if ok == 0:
	    if self.Cache.Friends != {}:
		print
		print "Warning: Using cached friend groups data. If you have recently updated your"
		print "list of groups, double-check these results!"
		print
	    return

	self.process_getfriendgroups()


    def cli_consolecmd(self, cmds):
        "Send and process console commands."

        print "Processing commands..."
        print

        self.console_command(cmds)
        ok = self.parse_return("Commands")
        if ok == 0:
            return 0

        print barline

        rvals = self.Got["results"]
        for n in range(len(rvals)):
            r = rvals[n]
            print "Command: %s (%s)" % (" ".join(cmds[n]), ("FAILED", "executed")[r["success"]])
            print
            last_ttype = ""
            for (ttype, text) in r["output"]:
                if last_ttype == ttype:
                    print text
                else:
                    if last_ttype in ("info", "error"):
                        print
                    if ttype == "info":
                        print
                        print "INFO:"
                    elif ttype == "error":
                        print
                        print "ERROR:"
                    print text
                last_ttype = ttype    
            print barline            

    # -----
    # Checkfriends mode.
    # -----

    def prepare_checkfriends(self):
	"Process friend group information prior to checkfriends invocation."

	if self.Params.has_key("mask") or self.CheckGroups == []:
	    return
	if self.GottenFriends == 0:
	    self.cli_getfriendgroups()
	if self.Cache.Friends == {}:
	    return

	mask = 0
	fdict = {}
	for k in self.Cache.Friends.keys():
	    fdict[string.lower(k)] = int(self.Cache.Friends[k])
	for k in self.CheckGroups:
	    for g in fdict.keys():
		if k == g:
		    mask = mask | fdict[g]
		    break
	if mask != 0:
	    self.Params["mask"] = str(mask)


    def checkfriends_event(self):
	"Event called by checkfriends scheduler."

	self.cli_checkfriends(1)
	try:
	    if self.Got["new"] == "1":
		print "\n        >> You have new LiveJournal friend updates. <<\n"
	except KeyError:
	    pass


    def checkfriends_loop(self):
	"Run in the background, checking for friend updates."

	while 1:
	    self.checkfriends_event()
	    try:
		n = int(self.Got["interval"])
	    except ValueError:
		n = 0
	    except KeyError:
		n = 0
	    if n > self.CheckDelay:
		time.sleep(n)
	    else:
		time.sleep(self.CheckDelay)


    # -----
    # Post editing.
    # -----

    def edit_post(self):
	"Edit a post."

	try:
	    dfile = self.Params["draft_file"]
	    timestr = string.join((string.split(dfile, '_'))[-2:], '_')
	except KeyError:

	    # -- No drafts file, so we have a new post. The timestamp is
	    #    always the present.

	    timetuple = time.localtime(time.time())
	    timestr = time.strftime("%Y%m%d_%H%M%S", timetuple)
	    ok = self.make_draft_file(timestr)
	    if ok == 0:
		return
	    dfile = self.Params["draft_file"]

	    # -- Since we're doing a new drafts file, we should populate
	    #    it with the privacy line if it makes sense.

	    if self.Params.has_key("showperms"):
		try:
		    hdr_str = ""
		    if self.Params["security"] == "usemask":
			if self.Params["allowmask"] == "1":
			    hdr_str = "[ To my friends. ]\n"
			else:
			    mnum = int(self.Params["allowmask"])
			    olist = []
			    for k in self.sort_friendgroups():
				if (mnum & int(self.Cache.Friends[k])):
				    if self.GroupHeaders.has_key(k):
					olist.append(self.GroupHeaders[k])
				    else:
					olist.append("the " + k)
			    hdr_str = "[ To " + commalist(olist) + ". ]\n"
		    elif self.Params["security"] == "private":
			hdr_str = "[ Private only. ]\n"
		    if hdr_str != "":
			fh = open(dfile, 'w')
			fh.write(hdr_str)
			if self.Params.has_key("prop_opt_preformatted"):
			    fh.write("<P>\n")
			fh.write("\n")
			fh.close()
		except KeyError:
		    pass

	# -- Save the meta-data while we're at it, then invoke the editor.

	self.save_metadata(timestr)
	os.system(self.Params["editor"] + " " + dfile) 

    # -----
    # Utilities for user interaction menus.
    # -----

    def get_choice(self):
	"Get menu choice."
	
	res = sane_raw_input("Enter choice, and press return: ")
	print
	return res


    def do_quit(self):
	"Quit gracefully."

	self.save_session(1)
	self.Cache.save_cache(self.Params["user"])

	print "Thank you for using Charm."
	print
	sys.exit(0)

    def handle_interrupt(self, menu_str):
	"Handle a keyboard interrupt exception."

	print "\n\nReceived Ctrl-C or other break signal.\n"
	res = sane_raw_input("Do you want to quit (Y/N)? ")
	print
	res = string.lower(string.strip(res))
	if res in ("y", "yes"):
	    self.do_quit()
	else:
	    print "Returning to %s menu.\n" % (menu_str)


    def show_calendar(self, year, month):
	"Show calendar with post counts."

	print "Retrieving post counts for %s..." % \
	      (time.strftime("%B %Y", (year, month, 0, 0, 0, 0, 0, 0, 0)))
	print
	ok = self.cli_getdaycounts()
	if ok == 0:
	    print
	    print "Displaying ordinary calendar."
	    print
	    calendar.prmonth(year, month)
	    print
	    return

	bstr = "%(year)d-%(month)02d-" % vars()
	mmatrix = calendar.monthcalendar(year, month)
	weeks = len(mmatrix)

	whole_line = "+---------+---------+---------+---------+---------+---------+---------+"
	space_unit = "          "
	bar_unit =   "+---------"
	blank_unit = "         "	# one shorter

	print "  Sunday    Monday    Tuesday  Wednesday  Thursday  Friday   Saturday"

	# -- Handle the first week. We may have partial boxes.

	line = ""
	for d in range(7):
	    if mmatrix[0][d] == 0:
		line = line + space_unit
	    else:
		line = line + bar_unit
	print line + "+"

	for d in range(7):
	    if mmatrix[0][d] == 0:
		print blank_unit,
	    else:
		try:
		    dstr = "%02d" % mmatrix[0][d]
		    print "| %2d %-4s" % (mmatrix[0][d],
					  "(" + self.Got[bstr + dstr] + ")"),
		except KeyError:
		    print "| %2d     " % mmatrix[0][d],
	print "|"

	print whole_line

	# -- Do the middle weeks.

	for w in range(1, weeks - 1):
	    for d in range(7):
		try:
		    dstr = "%02d" % mmatrix[w][d]
		    print "| %2d %-4s" % (mmatrix[w][d],
					  "(" + self.Got[bstr + dstr] + ")"),
		except KeyError:
		    print "| %2d     " % mmatrix[w][d],
	    print "|"
	    print whole_line

	# -- Do the last week.

	for d in range(7):
	    if mmatrix[-1][d] == 0:
		break
	    else:
		try:
		    print "| %2d %-4s" % \
			  (mmatrix[-1][d],
			   "(" + self.Got[bstr + str(mmatrix[-1][d])] + ")"),
		except KeyError:
		    print "| %2d     " % mmatrix[-1][d],
	print "|"

	line = ""
	for d in range(7):
	    if mmatrix[-1][d] == 0:
		break
	    else:
		line = line + bar_unit
	print line + "+"
	print


    def set_pickw(self, pname):
	"Set a picture keyword, if we can."

	# -- We must do a case-insensitive compare (but picture keywords
	#    can be case-sensitive, so we can't just universally lowercase
	#    them when we first store them).

	for k in self.Cache.PicKws:
	    if pname == string.lower(k):
		self.Params["prop_picture_keyword"] = k
		break


    # -----
    # Parameter setting.
    # -----

    def set_mood(self, mood_text):
	"Set a mood, given text."

	mood_text = string.lower(string.strip(mood_text))
	self.Mood = mood_text

	# -- Wipe out the old moods.

	self.del_param("prop_current_mood")
	self.del_param("prop_current_moodid")

	if mood_text == "":
	    return

	# -- Do we have this mood pre-defined? If so, use ID. Otherwise, text.

	try:
	    mood_id = self.Cache.Moods[mood_text]
	    self.Params["prop_current_moodid"] = mood_id
	except KeyError:
	    self.Params["prop_current_mood"] = mood_text

	# -- Moods might correspond to picture keywords. Try to set.
	#    (If we don't match anything, no harm done.)

	self.set_pickw(mood_text)


    def set_security(self, pval):
	"Set security level to something."

	if pval == "public":
	    self.Params["security"] = "public"
	    self.del_param("allowmask")
	elif pval == "friends":
	    self.Params["security"] = "usemask"
	    self.Params["allowmask"] = "1"
	elif pval == "private":
	    self.Params["security"] = "private"
	    self.del_param("allowmask")
	else:
	    if self.GottenFriends == 0:
		self.cli_getfriendgroups()
	    if self.Cache.Friends != {}:
		tmp = string.lower(pval)
		for k in self.Cache.Friends.keys():
		    if tmp == string.lower(k):
			self.Params["security"] = "usemask"
			self.Params["allowmask"] = str(self.Cache.Friends[k])
			return
	    raise ValueError


    # -----
    # Saving and displaying posts.
    # -----

    def write_metadata(self, f):
	"Write out post meta-data to a filehandle."

	if self.Params.has_key("year"):
	    f.write("Date:      " + self.format_time() + "\n")
	else:
	    timetuple = time.localtime(time.time())
	    f.write("Date:      " + time.strftime("%Y-%m-%d %H:%M",
						  timetuple) + "\n")

	if self.Params.has_key("usejournal"):
	    f.write("Journal:   " + self.Params["usejournal"] + "\n")
	    if self.Params.has_key("poster"):
		f.write("Poster:    " + self.Params["poster"] + "\n")

	if self.Params.has_key("subject"):
	    f.write("Subject:   " + self.Params["subject"] + "\n")

	if self.Mood != "":
	    f.write("Mood:      " + self.Mood + "\n")

	if self.Params.has_key("prop_picture_keyword"):
	    f.write("Picture:   " + self.Params["prop_picture_keyword"] + "\n")

	if self.Params.has_key("prop_current_music"):
	    f.write("Music:     " + self.Params["prop_current_music"] + "\n")

	try:
	    if self.Params["security"] == "usemask":
		if self.Params["allowmask"] == "1":
		    f.write("Security:  friends\n")
		else:
		    f.write("Security:  custom\n");
		    mnum = int(self.Params["allowmask"])
		    olist = []
		    for k in self.sort_friendgroups():
			if (mnum & int(self.Cache.Friends[k])):
			    olist.append(k)
		    f.write("Friends:   " + commalist(olist) + "\n")
	    else:
		f.write("Security:  " + self.Params["security"] + "\n")
	except KeyError:
	    pass


    def write_post(self, f):
	"Write out a post to a filehandle."

	self.write_metadata(f)

	try:
	    d = open(self.Params["draft_file"], 'r')
	    f.write("\n")
	    line = d.readline()
	    while line != "":
		f.write(line)
		line = d.readline()
	    d.close()
	except:
	    pass


    def display_post(self):
	"Display a post, using a pager if one is available."

	print barline
	print

	try:
	    ppath = self.Params["pager"]
	except KeyError:
	    try:
		ppath = os.environ['PAGER']
	    except KeyError:
		ppath = ""

	if ppath == "":
	    ppath = "/usr/bin/more"

	# -- Make sure we can execute this pager before trying it.
	#    If not, we just splat the file to the screen. 
	#
	#    We have some system-specific things to handle here.
	#    Python doesn't implement popen() on the Mac, and pipes
	#    behave weirdly with Windows (per the Python FAQ).

	if os.access(ppath, os.F_OK | os.X_OK) == 0 or os.name == "mac":
	    f = sys.stdout
	    is_pipe = 0
	else:
	    if sys.platform == "win32":
		import win32pipe
		f = win32pipe.popen(ppath, "w")
	    else:
		f = os.popen(ppath, "w")
	    is_pipe = 1

	self.write_post(f)

	if is_pipe == 1:
	    f.close()

	print
	print barline
	print

    def save_post_file(self):
	"Save post to a file."

	print
	res = sane_raw_input("File name to save under: ")
	print

	afile = os.path.expanduser(res)
	try:
	    f = open(afile, 'w')
	except IOError:
	    print "Error writing to file."
	    return

	self.write_post(f)
	f.close()
	print "Saved."

    def make_archive(self, is_edit = 0, text_to_output = ""):
	"Archive current post."

	# -- Make sure we have the base archive directory, and then
	#    the subpaths if we need them.
	#
	#    If we have subdirs turned on, each journal type gets its
	#    own subdirectory.
	#
	#    If we can't create subdirectories, we use the upper level.

	adir = self.Params["archive_dir"]
	ok = create_dir(adir, "archive")
	if ok == 0:
	    return

	if self.Params["archive_subdirs"] == "1":
	    if self.Params.has_key("usejournal"):
		tmp = adir + "/" + self.Params["usejournal"]
	    else:
		tmp = adir + "/" + self.Params["user"]
	    ok = create_dir(tmp, "archive journal subdirectory")
	    if ok != 0:
		adir = tmp

	if self.Params["organize"] in ("year", "month"):
	    tmp = adir + "/" + self.Params["year"]
	    ok = create_dir(tmp, "archive year")
	    if ok != 0:
		adir = tmp

	if self.Params["organize"] == "month":
	    tmp = adir + "/" + self.Params["mon"]
	    ok = create_dir(tmp, "archive month")
	    if ok != 0:
		adir = tmp

	# -- Write out the new file. If we have a hierarchical directory
	#    structure, leave out the earlier date-parts of the filename.

	if self.Params["organize"] == "none":
	    afdate = self.Params["year"] + self.Params["mon"]
	elif self.Params["organize"] == "year":
	    afdate = self.Params["mon"]
	else:
	    afdate = ""

	afile = "%s/%s%s_%s%s" % (adir, afdate, self.Params["day"],
				  self.Params["hour"], self.Params["min"])

	if (is_edit == 1) and (self.Params["archive_overwrite"] != "1"):
	    timetuple = time.localtime(time.time())
	    afile = afile + "_ed" + time.strftime("%Y%m%d%H%M", timetuple)

	try:
	    f = open(afile, 'w')
	except IOError:
	    print "Error writing to archive file."
	    return

	if text_to_output == "":
	    self.write_post(f)
	else:
	    self.write_metadata(f)
	    f.write("\n")
	    f.write(text_to_output)
	    f.write("\n")
	f.close()
	

    # -----
    # Upload a post.
    # -----

    def go_post(self):
	"Send a post to the server."

	# -- Read in the draft file.

	ok = self.slurp_draft("Post")
	if ok == 0:
	    return 0

	# -- Save the time of the post, if we don't have one yet.
	#    That way we will always stamp when we tried to post (unless
	#    we're backdating).
	#    Save our session data, just in case.

	if self.Params.has_key("year") == 0:
	    timetuple = time.localtime(time.time())
	    self.populate_time(timetuple)
	    
	self.save_session()

	# -- Go post it. If we failed, just go back to posting mode.

	ok = self.cli_postevent()
	if ok == 0:
	    return 0
	else:
	    print "Post successful."

	# -- Archive, if we should.

	if self.getval("archive", "0") == "1":
	    self.make_archive(0)
	    print "Your posting has been archived."

	# -- Delete the old files. Clear out meta-data.

	self.clear_session()
	print "Draft cleared. Beginning new session."
	return 1

    # -----
    # Submit a post edit.
    # -----

    def go_edit(self):
	"Post an edited entry."

	# -- Save the session data.

	self.save_session()

	# -- Read in the draft file.

	ok = self.slurp_draft("Edit")
	if ok == 0:
	    return 0

	# -- If we are timestamping the edit, append that.

	if self.Params.has_key("edit_times") and self.Params["edit_times"] == "1": 
	    now = time.localtime(time.time())
	    if now[0] == int(self.Params["year"]):
		if now[1] == int(self.Params["mon"]) and \
		   now[2] == int(self.Params["day"]):
		    ts_text = time.strftime("[ Edited at %I:%M %p. ]", now)
		else:
		    ts_text = time.strftime("[ Edited on %B %d at %I:%M %p. ]", now)
	    else:
		ts_text = time.strftime("[ Edited on %B %d, %Y, at %I:%M %p. ]", now)
	    if self.Params.has_key("prop_opt_preformatted") and \
	       self.Params["prop_opt_preformatted"] == "1":
		self.Params["event"] = append_htblock(self.Params["event"],
						      "<I>" + ts_text + "</I>")
	    else:
		self.Params["event"] = "%s\n%s\n" % \
				       (self.Params["event"], ts_text)

	# -- Make an upload attempt.

	print "Sending edited post..."
	print

	ok = self.cli_editevent()
	if ok == 0:
	    return 0
	else:
	    print "Edit successful."

	# -- Archive editing, if we should.

	if self.Params["archive_edits"] == "1":
	    self.make_archive(1)
	    print "Your edited posting has been archived."

	# -- Delete the old files. Clear out meta-data.

	self.clear_session()
	self.Entries = []
	self.Entry = {}
	print "Draft cleared. Beginning new session."
	return 1


    # -----
    # Delete a post.
    # -----

    def go_delete(self):
	"Delete a post."

	print "Deleting..."
	print

	ok = self.cli_delevent()
	if ok == 0:
	    return 0

	# -- Once we're done we should return to blank-slate status.

	self.clear_session()
	self.Entries = []
	self.Entry = {}
	print "Deleted. Beginning new session."
	return 1


    # -----
    # Draft resumption.
    # -----

    def read_metadata(self, mfname):
	"Read and set meta-data from a file."

	try:
	    f = open(mfname, 'r')
	except IOError:
	    return			# no big loss.

	meta_in = {}
	line = f.readline()
	while line != "":
	    line = line[:-1]		# discard newline
	    if line == "":
		pass
	    elif line[0] == "#":	# comment line, discard
		pass
	    else:
		inpair = string.split(line, '=', 1)
		meta_in[inpair[0]] = inpair[1]
		if inpair[0] == "prop_current_mood":
		    self.Mood = inpair[1]
		elif inpair[0] == "prop_current_moodid":
		    for k in self.Cache.Moods.keys():
			if self.Cache.Moods[k] == inpair[1]:
			    self.Mood = k
			    break
	    line = f.readline()
	f.close()

	# -- We only allow ourselves to read what we know we could have
	#    written. Yes, a user could always do something weird here,
	#    but this is good enough.

	for k in meta_in.keys():
	    if k in Basic_MetaData:
		self.Params[k] = meta_in[k]


    def resume_draft(self, dfile, is_virgin = 0):
	"Resume working on a previous draft, given its filename."

	# -- Look for the file. Try the filename as specified, then look
	#    in the drafts directory.

	dfile = os.path.expanduser(dfile)

	if os.path.exists(dfile) == 1:
	    self.Params["draft_file"] = dfile
	else:
	    dfile = self.Params["draft_dir"] + "/" + dfile
	    if os.path.exists(dfile) == 1:
		self.Params["draft_file"] = dfile
	    else:
		print "Session unchanged. The specified draft file was not found."
		return 0

	# -- Find the base timestring.

	base_dfile = os.path.basename(dfile)
	timestr = string.join((string.split(base_dfile, '_'))[-2:], '_')

	# -- Clear out any meta-data that we might have previously had.

	if is_virgin == 0:
	    self.clear_metadata()

	# -- Look for the meta-data file in the same directory. Load it.

	mfname = os.path.dirname(dfile) + "/" + ".meta_" + timestr
	if os.path.exists(mfname) == 1:
	    self.read_metadata(mfname)

	print "Resuming previous draft of post."
	return 1


    # -----
    # User interaction menus.
    # -----

    def drafts_menu(self):
	"Display a menu of old drafts."

	print """
SELECT DRAFT

[f] Enter filename of draft.
[l] Select from a list of old drafts."""
	print
	res = self.get_choice()
	dfile = ""

	if res in ("F", "f"):
	    dfile = sane_raw_input("Enter filename of draft: ")
	    print
	    dfile = string.strip(dfile)

	elif res in ("L", "l"):
	    try:
		import dircache
		import fnmatch
	    except ImportError:
		print "Your Python installation lacks the necessary modules."
		return
	    list = dircache.listdir(self.Params["draft_dir"])
	    fkeys = {}
	    for n in list:
		if fnmatch.fnmatch(n, ".meta_????????_??????"):
		    try:
			mfile = open(self.Params["draft_dir"] + "/" + n)
			fkeys[n[6:]] = "(no subject)"
			line = mfile.readline()
			while line != "":
			    line = line[:-1]
			    if line == "":
				pass
			    elif line[0] == "#":
				pass
			    else:
				inpair = string.split(line, '=', 1)
				if inpair[0] == "subject":
				    fkeys[n[6:]] = inpair[1]
				    break
			    line = mfile.readline()
			mfile.close()
		    except IOError:
			pass

	    klist = fkeys.keys()
	    klist.sort()
	    i = 1
	    for k in klist:
		if i < 10:
		    print "%d. " % (i),
		else:
		    print "%d." % (i),
		print k[:4] + "-" + k[4:6] + "-" + k[6:8],
		print k[9:11] + ":" + k[11:13] + ":" + k[13:15],
		if (len(fkeys[k]) > 45):
		    print truncstr_more(fkeys[k], 45)
		else:
		    print fkeys[k]
		i = i + 1

	    print
	    res = sane_raw_input("Enter draft number: ")
	    print
	    try:
		n = int(res)
	    except ValueError:
		n = 0
	    if n < 1 or n > len(klist):
		print "Invalid choice. You need to choose a valid draft."
	    else:
		dfile = "draft_" + klist[n - 1]

	else:
	    print "That is not a valid option."

	if dfile != "":
	    self.save_session(1)	# save meta-data, since it'll be wiped
	    repeat_ok = self.resume_draft(dfile)
	    while repeat_ok:
		try:
		    repeat_ok = self.post_menu()
		except KeyboardInterrupt:
		    self.handle_interrupt("posting")


    def username_menu(self):
	"Display a username menu."

	print
	print "SELECT USERNAME"
	print

	i = 1
	klist = self.Logins.keys()
	for k in klist:
	    print "%d. %s" (i, k)
	    i = i + 1

	print
	res = sane_raw_input("Enter username number: ")
	print

	try:
	    n = int(res)
	except ValueError:
	    n = 0
	if n < 1 or n > len(klist):
	    print "Invalid choice. You need to choose a valid username."
	    return 0
	else:
	    self.Params["user"] = klist[n - 1]
	    self.Params["hpassword"] = self.Logins[klist[n - 1]]

	return 1


    def select_time(self):
	"Set the posting time."

	print """
The date and time should be entered in YYYY-MM-DD HH:MM format.
The time is 24-hour time (00-23 for midnight - 11 pm).
Just press return without entering anything, to set this to the current time.
"""
	dstr = sane_raw_input("Enter date and time: ")
	print

	dstr = string.strip(dstr)
	if dstr == "":
	    timetuple = time.localtime(time.time())
	else:
	    try:
		timetuple = time.strptime(dstr, "%Y-%m-%d %H:%M")
	    except ValueError:
		print "Time format error. Time not changed."
		return

	self.populate_time(timetuple)
	

    def moodlist_menu(self):
	"Select mood from list."

	klist = self.Cache.Moods.keys()	# must copy, sort() is in-place
	klist.sort()
	column_table(klist, 4)

	print
	res = sane_raw_input("Enter mood number: ")
	print

	try:
	    n = int(res)
	except ValueError:
	    n = 0
	if n < 1 or n > len(klist):
	    print "Invalid choice. Mood unchanged."
	else:
	    mood_text = klist[n - 1]
	    self.set_mood(mood_text)
	    print "Mood set to %s." % (mood_text)


    def mood_menu(self):
	"Mood selection menu."

	print """
MOOD SELECTION MENU

[n] No mood.
[l] Select current mood from pre-defined list.
[o] Select other mood."""
	print
	res = self.get_choice()

	if res in ("N", "n"):
	    self.set_mood("")
	    print "Selected no mood."
	elif res in ("O", "o"):
	    mood_text = sane_raw_input("Mood: ")
	    self.set_mood(mood_text)
	    print
	    print "Mood selected."
	elif res in ("L", "l"):
	    if self.Cache.Moods == {}:
		print "No pre-defined moods are available."
	    else:
		self.moodlist_menu()
	else:
	    print "That is not a valid option."


    def pickw_menu(self):
	"Select picture keyword from list."

	print
	print "PICTURE KEYWORD SELECTION"
	print
	print "0. (none)"
	i = 1
	for k in self.Cache.PicKws:
            print "%d. %s" % (i, k)
	    i = i + 1
	print
	res = self.get_choice()

	try:
	    n = int(res)
	except ValueError:
	    n = -1
	if n == 0:
	    self.del_param("prop_picture_keyword")
	    print "No picture keyword selected."
	elif n < 0:
	    print "Invalid choice. Selection unchanged."
	else:
	    try:
		self.Params["prop_picture_keyword"] = self.Cache.PicKws[n - 1]
		print "Selected " + self.Cache.PicKws[n - 1] + " picture."
	    except IndexError:
		print "Invalid choice. Selection unchanged."


    def option_menu(self):
	"Miscellaneous option menu."

	print
	print "MISCELLANEOUS OPTIONS"
	print
	val_preformat = 0
	print "[a] Change auto-format option to:",
	try:
	    if self.Params["prop_opt_preformatted"] == "1":
		val_preformat = 1
		print "on (post will be formatted for you)"
	    else:
		print "off (format your post yourself)"
	except KeyError:
	    print "off (format your post yourself)"
	val_backdate = 0
	print "[b] Change backdate option to:",
	try:
	    if self.Params["prop_opt_backdated"] == "1":
		val_backdate = 1
		print "no backdating"
	    else:
		print "backdating on"
	except KeyError:
	    print "backdating on"
	val_nocom = 0
	print "[c] Change comments option to:",
	try:
	    if self.Params["prop_opt_nocomments"] == "1":
		val_nocom = 1
		print "comments allowed"
	    else:
		print "comments disallowed"
	except KeyError:
	    print "comments disallowed"
	val_noemail = 0	    
	print "[e] Change email option to:",
	try:
	    if self.Params["prop_opt_noemail"] == "1":
		val_noemail = 1
		print "email comments"
	    else:
		print "do not email comments"
	except KeyError:
	    print "do not email coments"
	print "[r] Return to the posting menu."
	print
	res = self.get_choice()

	if res in ("A", "a"):
	    if val_preformat == 0:
		self.Params["prop_opt_preformatted"] = "1"
		print "Auto-format turned off."
	    else:
		del self.Params["prop_opt_preformatted"]
		print "Auto-format turned on."
	elif res in ("B", "b"):
	    if val_backdate == 0:
		self.Params["prop_opt_backdated"] = "1"
		print "Post will be backdated."
	    else:
		del self.Params["prop_opt_backdated"]
		print "Post will not be backdated."
	elif res in ("C", "c"):
	    if val_nocom == 0:
		self.Params["prop_opt_nocomments"] = "1"
		print "Comments will be disallowed."
	    else:
		del self.Params["prop_opt_nocomments"]
		print "Comments will be allowed."
	elif res in ("E", "e"):
	    if val_noemail == 0:
		self.Params["prop_opt_noemail"] = "1"
		print "Comments will not be emailed."
	    else:
		del self.Params["prop_opt_noemail"]
		print "Comments will be emailed."
	elif res in ("R", "r"):
	    pass
	else:
	    print "That is not a valid option."


    def journal_menu(self, changepic = 0):
	"Journal selection menu."

	print 
	print "JOURNAL SELECTION MENU"
	print
	print "0. (" + self.Params["user"] + ") -- default"
	i = 1
	for k in self.Cache.Journals:
            print "%d. %s" % (i, k)
	    i = i + 1
	print
	res = self.get_choice()

	try:
	    n = int(res)
	except ValueError:
	    n = -1
	if n == 0:
	    self.del_param("usejournal")
	    print "Selected default journal (your own)."
	elif n < 0:
	    print "Invalid choice. Selection unchanged."
	else:
	    try:
		jname = self.Cache.Journals[n - 1]
		self.Params["usejournal"] = jname
		print "Selected %s." % (jname)
		# -- If we don't have a picture keyword set, and this
		#    journal has a valid default picture, set that.
		if changepic and self.CommPics.has_key(jname) and \
		   not self.Params.has_key("prop_picture_keyword"):
		    self.set_pickw(self.CommPics[jname])
	    except IndexError:
		print "Invalid choice. Selection unchanged."

    def friendgroup_menu(self):
	"Select friend group for security permissions."

	# -- Go download the friend groups if we don't have them already.

	if self.GottenFriends == 0:
	    self.cli_getfriendgroups()

	# -- Check for case of no friend groups defined.

	self.Params["security"] = "usemask"
	
	if self.Cache.Friends == {}:
	    self.Params["allowmask"] = "1"
	    print "No friend groups defined."
	    print "Security level set to friends only."
	    return

	# -- Otherwise show menu.

	print
	print "CHOOSE FRIEND GROUP"
	print
	print "0. All friends"

	i = 1
	klist = self.sort_friendgroups()
	for k in klist:
            print "%d. %s" % (i, k)
	    i = i + 1

	print
	res = sane_raw_input("Enter friend group number: ")
	print

	try:
	    n = int(res)
	except ValueError:
	    n = -1
	if n < 0 or n > len(klist):
	    print "Invalid choice. Security level defaulting to all friends."
	    self.Params["allowmask"] = "1"
	elif n == 0:
	    print "Security level set to all friends."
	    self.Params["allowmask"] = "1"
	else:
	    if (self.Params.has_key("allowmask") != 0) and \
	       (self.Params["allowmask"] != "1"):
		mnum = int(self.Params["allowmask"]) | \
		       int(self.Cache.Friends[klist[n - 1]])
		self.Params["allowmask"] = str(mnum)
		print "Security level set to custom:",
		olist = []
		for k in self.sort_friendgroups():
		    if (mnum & int(self.Cache.Friends[k])):
			olist.append(k)
		print commalist(olist)
	    else:
		self.Params["allowmask"] = self.Cache.Friends[klist[n - 1]]
		print "Security level set to custom (friend group %s) only." \
		      % (klist[n - 1])


    def security_menu(self):
	"Security permissions level menu."

	print """
SET SECURITY PERMISSIONS FOR POST

[d] Set security level to public (default).
[f] Set security level to friends only.
[c] Set security level to custom (select friend groups).
[p] Set security level to private."""
	print
	res = self.get_choice()

	if res in ("D", "d"):
	    self.Params["security"] = "public"
	    self.del_param("allowmask")
	    print "Security level set to public."
	elif res in ("F", "f"):
	    self.Params["security"] = "usemask"
	    self.Params["allowmask"] = "1"
	    print "Security level set to friends only."
	elif res in ("C", "c"):
	    self.friendgroup_menu()
	elif res in ("P", "p"):
	    self.Params["security"] = "private"
	    self.del_param("allowmask")
	    print "Security level set to private."
	else:
	    print "That is not a valid option."	    


    def common_menu(self, header_text, edit_only = 0):
	"Common menu between editing previous post, and regular posting."

	print
	print header_text
	print

	print "[e] Edit text of current post."

	if edit_only == 0 and self.Cache.Journals != []:
	    print "[j] Change journal to post in:",
	    try:
		print self.Params["usejournal"]
	    except KeyError:
		print "(" + self.Params["user"] + ")"

	print "[s] Change subject:",
	if self.Params.has_key("subject"):
	    if len(self.Params["subject"]) > 45:
		print truncstr_more(self.Params["subject"], 45)
	    else:
		print self.Params["subject"]
	else:
	    print "(none)"

	print "[m] Change mood:",
	if self.Mood == "":
	    print "(none)"
	else:
	    print self.Mood

	if self.Cache.PicKws != []:
	    print "[k] Change picture keyword:",
	    print self.getval("prop_picture_keyword", "(default)")

	print "[a] Change current music:",
        if self.Params.has_key("autodetect") == 1:
            print self.autodetect_music()
        else:
            print self.getval("prop_current_music")

	print "[p] Change security permissions:",
	try:
	    if self.Params["security"] == "usemask":
		if self.Params["allowmask"] == "1":
		    print "friends"
		else:
		    mnum = int(self.Params["allowmask"])
		    olist = []
		    for k in self.sort_friendgroups():
			if (mnum & int(self.Cache.Friends[k])):
			    olist.append(k)
		    print "custom (" + commalist(olist) + ")"
	    else:
		print self.Params["security"]
	except KeyError:
	    print "public"

	print "[o] Change other options:",
	if self.Params.has_key("prop_opt_preformatted"):
	    opt_list = [ "don't auto-format" ]
	else:
	    opt_list = [ "auto-format" ]
	if self.Params.has_key("prop_opt_nocomments"):
	    opt_list.append("no comments")
	else:
	    if self.Params.has_key("prop_opt_noemail"):
		opt_list.append("comments okay (but not emailed)")
	    else:
		opt_list.append("comments okay")
	if self.Params.has_key("prop_opt_backdated"):
	    opt_list.append("backdated")
	print string.join(opt_list, ", ") 

	print "[t] Change time and date of current post:",
	if self.Params.has_key("year"):
	    print self.format_time()
	else:
	    print "(posting time)"

	print "---"

	dsize = 0
	if self.Params.has_key("draft_file"):
	    try:
		dsize = (os.stat(self.Params["draft_file"]))[stat.ST_SIZE]
	    except OSError:
		dsize = 0
	if dsize == 0:
	    print "[d] Display current post (no text yet)."
	else:
	    print "[d] Display current post (%d bytes)." % (dsize)

	if edit_only == 0:
	    print "[u] Update (send the current post)."
	else:
	    print "[u] Update (submit the edited post)."
	    print "[w] Wipe out this post (delete it from the server)."

#	print "[v] Validate/spellcheck this post."
	print "[c] Copy current post to file."
	print "[r] Return to main menu."
	print "[q] Quit."
#	print "[z] Dump debugging info."
	print
	res = self.get_choice()

	if res in ("R", "r"):
	    return 0
	elif res in ("Q", "q"):
	    self.do_quit()
	elif res in ("Z", "z"):
	    self.dump_debug_info()
	elif res in ("D", "d"):
	    self.display_post()
#	elif res in ("V", "v"):
#	    self.spellcheck()
	elif res in ("C", "c"):
	    self.save_post_file()
	elif res in ("E", "e"):
	    self.edit_post()
	elif res in ("J", "j") and edit_only == 0 and \
	     self.Cache.Journals != []:
	    self.journal_menu(not edit_only)
	elif res in ("M", "m"):
	    self.mood_menu()
	elif res in ("K", "k") and self.Cache.PicKws != []:
	    self.pickw_menu()
	elif res in ("O", "o"):
	    self.option_menu()
	elif res in ("P", "p"):
	    self.security_menu()
	elif res in ("T", "t"):
	    self.select_time()
	elif res in ("A", "a"):
	    m_text = sane_raw_input("Current music: ")
	    m_text = string.strip(m_text)
	    if m_text == "":
		self.del_param("prop_current_music")
		print "Selected: no current music."
	    else:
		self.Params["prop_current_music"] = m_text
		print "Selected current music."
            self.del_param("autodetect")
	    print
	elif res in ("S", "s"):
	    subj_text = sane_raw_input("Subject: ")
	    self.Params["subject"] = subj_text[:255] # truncate to max length
	    print
	    if len(subj_text) > 255:
		print "Subject changed. WARNING: Truncated to 255 characters."
	    else:
		print "Subject changed."
	elif res in ("U", "u"):
	    if edit_only == 0:
		ok = self.go_post()
	    else:
		ok = self.go_edit()
	    if ok == 1:		# successful, don't repeat menu
		return 0
	elif res in ("W", "w") and edit_only == 1:
	    ok = self.go_delete()
	    if ok == 1:
		return 0
	else:
	    print "That is not a valid option."

	return 1


    def post_menu(self):
	"Post menu."

	return self.common_menu("POST MENU", 0)


    # -----
    # Console commands.
    # -----

    def console_menu(self):
        "Console commands main menu."

        print """
ADMINISTRATIVE CONSOLE MENU

[f] List, add, or remove friends.
[c] Add or remove community users.
[b] Ban or unban users from your journal or community.
[s] Grant or revoke posting access to a shared journal.
[r] Return to main menu.
[q] Quit.
"""
        res = self.get_choice()

        if res in ("R", "r"):
            return 0
        elif res in ("Q", "q"):
            self.do_quit()
        elif res in ("Z", "z"):
            self.dump_debug_info()
        elif res in ("B", "b"):
            self.ban_menu()
        elif res in ("C", "c"):
            self.community_menu()
        elif res in ("F", "f"):
            self.friends_menu()
        elif res in ("S", "s"):
            self.sharedjour_menu()
        else:
            print "That is not a valid option."

        return 1


    def friends_menu(self):
        "Friends management menu."

        print """
FRIENDS MANAGEMENT MENU

[l] List friends.
[f] Add someone as a friend.
[u] Un-friend someone.
[r] Return to administrative menu.
"""
        res = self.get_choice()

        if res in ("R", "r"):
            return
        elif res in ("L", "l"):
            cmd = [ "friend", "list" ]
            self.cli_consolecmd( [ cmd ] )
        elif res in ("F", "f"):
            fname = sane_raw_input("Enter the username of your new friend: ")
            cmd = [ "friend", "add", fname.strip() ]
            print
            print "These are your current friend groups."
            print
            print "0. Don't put this friend in a group."
            if self.GottenFriends == 0:
                self.cli_getfriendgroups()
            i = 1
            klist = self.sort_friendgroups()
            for k in klist:
                print "%d. %s" % (i, k)
                i = i + 1
            print
            ginput = sane_raw_input("Group to place friend in: ")
            print
            try:
                gnum = int(ginput)
            except ValueError:
                gnum = -1
            if gnum < 0 or gnum > len(klist):
                print "Invalid choice. Defaulting to no group."
                print
            elif gnum > 0:
                cmd.append(klist[gnum - 1])
            bgcol = sane_raw_input("Background color to use for friend: ")
            fgcol = sane_raw_input("Foreground color to use for friend: ")
            if valid_hexcolor(fgcol):
                cmd.append("fgcolor=%s" % (fgcol))
            if valid_hexcolor(bgcol):
                cmd.append("bgcolor=%s" % (bgcol))
            print
            self.cli_consolecmd( [ cmd ] )
        elif res in ("U", "u"):
            fname = sane_raw_input("Enter the username to un-friend: ")
            print
            cmd = [ "friend", "remove", fname.strip() ]
            self.cli_consolecmd( [ cmd ] )
        else:
            print "That is not a valid option."


    def community_menu(self):
        "Community management: add/remove users."

        print """
COMMUNITY MANAGEMENT MENU

[a] Add users to a community.
[u] Unsubscribe users from a community.
[r] Return to administrative menu.
"""
        res = self.get_choice()

        if res in ("R", "r"):
            return
        elif res in ("A", "a", "U", "u"):
            cname = sane_raw_input("Enter the community name: ")
            print
            cmd = [ "community", cname.strip() ]
            if res in ("A", "a"):
                cmd.append("add")
            else:
                cmd.append("remove")
            unames = sane_raw_input("Enter usernames (separated by spaces): ")
            print
            clist = []
            for u in unames.split(" "):
                c = cmd[:]
                c.append(u)
                clist.append(c)
            self.cli_consolecmd(clist)
        else:
            print "That is not a valid option."


    def ban_menu(self):
        "Ban/unban users from a journal or community."

        print """
BAN MANAGEMENT MENU

[b] Ban users from your journal.
[u] Unban users from your journal.
[e] Exile (ban) users from a community.
[a] Allow (unban) users back into a community.
[r] Return to administrative menu.
"""
        res = self.get_choice()
        
        if res in ("R", "r"):
            return
        elif res in ("B", "b", "U", "u", "E", "e", "A", "a"):
            if res in ("B", "b", "E", "e"):
                cmd = [ "ban_set" ]
            else:
                cmd = [ "ban_unset" ]
            if res in ("E", "e", "A", "a"):
                cname = sane_raw_input("Enter the community name: ")
                cargs = [ "from", cname.strip() ]
                print
            else:
                cargs = []
            unames = sane_raw_input("Enter usernames (separated by spaces): ")
            print
            clist = []
            for u in unames.split(" "):
                c = cmd[:]
                c.append(u)
                c += cargs
                clist.append(c)
            self.cli_consolecmd(clist)
        else:
            print "That is not a valid option."


    def sharedjour_menu(self):
        "Grant or revoke posting access to shared journal."

        print """
SHARED JOURNAL MANAGEMENT MENU

[a] Allow users to post in a shared journal.
[d] Disallow users from posting in a shared journal.
[r] Return to administrative menu.
"""
        res = self.get_choice()

        if res in ("R", "r"):
            return
        elif res in ("A", "a", "D", "d"):
            jname = sane_raw_input("Enter the shared journal name: ")
            print
            cmd = [ "shared", jname.strip() ]
            if res in ("A", "a"):
                cmd.append("add")
            else:
                cmd.append("remove")
            unames = sane_raw_input("Enter usernames (separated by spaces): ")
            print
            clist = []
            for u in unames.split(" "):
                c = cmd[:]
                c.append(u)
                clist.append(c)
            self.cli_consolecmd(clist)
        else:
            print "That is not a valid option."
            
    # -----
    # Edit a previous post.
    # -----

    def edit_previous_post(self):
	"Edit previous post."

	ok = self.cli_getevents_one()
	if ok == 0:
	    return 1			# we DO want to repeat on failure

	if self.Params.has_key("allowmask"):
	    if self.GottenFriends == 0:
		self.cli_getfriendgroups() # needed for permissions display 

	repeat_ok = 1
	while repeat_ok:
	    try:
		repeat_ok = self.common_menu("EDIT PREVIOUS JOURNAL ENTRY", 1)
	    except KeyboardInterrupt:
		self.handle_interrupt("edit")

	return 0


    # -----
    # Select old post to edit.
    # -----

    def pick_entry(self, eobj):
	"Given an entry object, choose it."

	self.Entry = eobj


    def pick_from_list(self):
	"Pick something from the list of entries."

	llist = len(self.Entries)
	if llist == 0: 
	    print "No entries available."
	    return
	elif llist == 1:
	    print "One entry returned. Selecting."
	    self.pick_entry(self.Entries[0])
	    return

	print
	print "SELECT FROM LIST"
	print

	i = 1
	for elem in self.Entries:
	    if i < 10:
                print "%d. " % (i),
	    else:
                print "%d." % (i),
	    print elem["time"] + " " + elem["subject"]
	    i = i + 1

	print
	res = sane_raw_input("Enter entry number: ")
	print

	try:
	    n = int(res)
	except ValueError:
	    n = 0
	if n < 1 or n > llist:
	    print "Invalid choice."
	else:
	    print "Selected."
	    self.pick_entry(self.Entries[n - 1])
	

    def pick_lastn(self):
	"Select from last N entries."

	res = sane_raw_input("Enter the number of entries to retrieve (max of 50): ")
	print

	try:
	    n = int(res)
	except ValueError:
	    n = 0
	if n < 1 or n > 50:
	    print "You must pick a number between 1 and 50."
	    return

	self.Params["selecttype"] = "lastn"
	self.Params["howmany"] = str(n)
	ok = self.cli_getevents_list([ "howmany" ])
	if ok == 0:
	    return
	self.pick_from_list()


    def select_date(self):
	"Choose a date, with the aid of a calendar."

	# -- Display this month's calendar, as a handy aid.

	calendar.setfirstweekday(calendar.SUNDAY)
	timetuple = time.localtime(time.time())
	print
	calendar.prmonth(timetuple[0], timetuple[1])
	print
	print

	# -- Repeat the menu.

	repeat_ok = 1
	while repeat_ok:
	    print """\
You can enter the date in YYYY-MM-DD format to select a date, or YYYY-MM to
see a calendar of that month, which will show the number of posts per day."""
	    print
	    dstr = sane_raw_input("Enter date: ")
	    print
	    dstr = string.strip(dstr)
	    if dstr == "":
		print "Invalid date format."
		raise ValueError
	    try:
		ttup = time.strptime(dstr, "%Y-%m-%d")
		repeat_ok = 0
	    except ValueError:
		try:
		    ttup = time.strptime(dstr, "%Y-%m")
		except ValueError:
		    print "Invalid date format."
		    raise
		self.show_calendar(ttup[0], ttup[1])
		print
	return ttup


    def pick_date(self):
	"Select from entries on a certain date."

	try:
	    ttup = self.select_date()
	except ValueError:
	    return

	self.Params["year"] = time.strftime("%Y", ttup)
        self.Params["month"] = time.strftime("%m", ttup)
        self.Params["day"] = time.strftime("%d", ttup)
	self.Params["selecttype"] = "day"
	ok = self.cli_getevents_list([ "year", "month", "day" ])
	if ok == 0:
	    return
	self.pick_from_list()


    def pick_most_recent(self):
	"Select the most recent post."

	self.Params["selecttype"] = "one"
	self.Params["itemid"] = "-1"
	ok = self.cli_getevents_list([ "itemid" ])
	if ok == 0:
	    return

	if len(self.Entries) > 1:
	    print "Error: Server returned more than one post."
	    return
	self.pick_entry(self.Entries[0])


    def pick_edit_menu(self):
	"Select old post to edit."

	print
	print "SELECT PREVIOUS POST"
	print

	if self.Cache.Journals != []:
	    print "[j] Change journal to take post from:",
	    try:
		print self.Params["usejournal"]
	    except KeyError:
		print "(" + self.Params["user"] + ")"

	print "[l] Select the last (most recent) post."
	print "[n] Select from the last N number of entries."
	print "[d] Select from entries posted on a certain date."

	if self.Entry != {}:
	    print "[e] Edit post:",
	    print self.Entry["time"],
	    if len(self.Entry["subject"]) > 45:
		print truncstr_more(self.Entry["subject"], 45)
	    else:
		print self.Entry["subject"]

	print "[r] Return to main menu."
	print "[q] Quit."
	print
	res = self.get_choice()

	if res in ("R", "r"):
	    return 0
	elif res in ("Q", "q"):
	    self.do_quit()
	elif res in ("Z", "z"):
	    self.dump_debug_info()
	elif res in ("J", "j") and self.Cache.Journals != []:
	    self.journal_menu()
	elif res in ("L", "l"):
	    self.pick_most_recent()
	elif res in ("N", "n"):
	    self.pick_lastn()
	elif res in ("D", "d"):
	    self.pick_date()
	elif res in ("E", "e") and self.Entry != {}:
	    repeat_ok = self.edit_previous_post()
	    return repeat_ok
	else:
	    print "That is not a valid option."
	return 1


    # -----
    # Mass-archive posts.
    # -----

    def copy_event_metadata(self, n, pcmax):
	"Populate data from one of many events."

	try:
	    self.Params["itemid"] = self.Got["events_%d_itemid" % (n)]
	except KeyError:
	    return 0

	# -- We default time to now on an error, but this can be dangerous
	#    if we're using this retrieval for archive purposes.

	try:
	    timetuple = time.strptime(self.Got["events_%d_eventtime" % (n)],
				      "%Y-%m-%d %H:%M:%S")
	except ValueError:
	    try:
		timetuple = time.strptime(self.Got["events_%d_eventtime" % (n)],
					  "%Y-%m-%d %H:%M")
	    except ValueError:
		timetuple = time.localtime(time.time())

	self.copy_net_data("events_%d_" % (n),
			   [ "poster", "security", "allowmask", "subject" ])

	for x in range(1, pcmax + 1):
	    try:
		if self.Got["prop_%d_itemid" % (x)] == self.Params["itemid"]:
		    self.Params["prop_" + self.Got["prop_%d_name" % (x)]] = \
					self.Got["prop_%d_value" % (x)]
	    except KeyError:
		pass

	self.event_common_data(timetuple)
	return 1
	

    def archive_events(self, journal_name):
	"Archive multiple events retrieved from the network. Return errors."

	try:
	    ecount = int(self.Got["events_count"])
	except KeyError:
	    raise ValueError
	if ecount < 1:
	    raise ValueError

	try:
	    pcstr = self.Got["prop_count"]
	    try:
		pcmax = int(pcstr)
	    except ValueError:
		raise ValueError
	except KeyError:
	    pcmax = 0

	err_n = 0
	for n in range(1, ecount + 1):
	    self.clear_metadata()
	    if journal_name != "":
		self.Params["usejournal"] = journal_name
	    ok = self.copy_event_metadata(n, pcmax)
	    if ok == 0:
		err_n = err_n + 1
	    else:
		self.make_archive(0, urllib.unquote_plus(self.Got["events_%d_event" % (n)]))
	return err_n


    def archive_days(self, s_year, s_mon, s_day, e_day, day_counts):
	"Archive posts in a day range (in a single month)."

	# -- Save what journal we're in, because otherwise it will get
	#    wiped by metadata wipeout.

	try:
	    save_journal = self.Params["usejournal"]
	except KeyError:
	    save_journal = ""

	count = 0
	err_n = 0
	bstr = str(s_year) + ("-%02d-" % s_mon)

	for n in range(s_day, e_day + 1):
	    dstr = "%02d" % n
	    if day_counts.has_key(bstr + dstr):
		try:
		    self.cli_getevents_day( (s_year, s_mon, n, 0, 0, 0, 0, 0, 0) )
		    new_errs = self.archive_events(save_journal)
		    print "  %s%s:" % (bstr, dstr),
		    if new_errs > 0:
			err_n = err_n + new_errs
			print "PARTIAL FAILURE. Errors encountered on %d of %s"% (new_errs, day_counts[bstr + dstr]),
		    else:
			print day_counts[bstr + dstr],
		    if day_counts[bstr + dstr] == "1":
			print "post."
		    else:
			print "posts."
		    count = count + int(day_counts[bstr + dstr]) - new_errs
		except IOError:
		    print "  %s%s: FAILED. Network error." % (bstr, dstr)
		    err_n = err_n + int(day_counts[bstr + dstr])
		except ValueError:
		    print "  %s%s: FAILED. No posts retrieved." % (bstr, dstr)
		    err_n = err_n + int(day_counts[bstr + dstr])
		if save_journal != "":
		    self.Params["usejournal"] = save_journal
	return (count, err_n)


    def mass_archive(self):
	"Given two dates, archive posts between those dates, inclusively."

	# -- Check values. If we don't have an end date, it defaults to today.

	try:
	    stt = self.Params["start_ttup"]
	except KeyError:
	    print "You must specify a start date for archival."
	    return

	try:
	    ett = self.Params["end_ttup"]
	except KeyError:
	    ett = time.localtime(time.time())

	print "Retrieving post counts..."

	ok = self.cli_getdaycounts()
	if ok == 0:
	    return

	# -- Save this so it doesn't get overwritten by later network ops.

	save_counts = {}
	for k in self.Got.keys():
	    save_counts[k] = self.Got[k]

	# -- Get the start date and end dates right, reversing them if
	#    need be.

	ok = 1
	if stt[0] > ett[0]:
	    ok = 0
	elif stt[0] == ett[0]:
	    if stt[1] > ett[1]:
		ok = 0
	    elif stt[1] == ett[1]:
		if stt[2] > ett[2]:
		    ok = 0
	if ok == 0:
	    mtt = stt
	    stt = ett
	    ett = mtt

	# -- Figure out the number of days in the first month. We iterate
	#    from the start day to the end of the month, then into the
	#    next month, doing as many complete months as need be, up until
	#    the last month, which is a partial month. Change of year also
	#    presents a problem.

	if stt[0] == ett[0] and stt[1] == ett[1]:
	    print
	    print "Archiving..."
	    val = self.archive_days(stt[0], stt[1], stt[2], ett[2], save_counts)
	else:
	    mstr = time.strftime("%B", (stt[0], stt[1], 1, 0, 0, 0, 0, 0, 0))
	    print
	    print "Archiving from %s %d, %d..." % (mstr, stt[2], stt[0])
	    val = self.archive_days(stt[0], stt[1], stt[2],
				    calendar.monthrange(stt[0], stt[1])[1],
				    save_counts)
	count = val[0]
	err_n = val[1]

	# -- If the end date is in the next year or beyond, get the entries
	#    for the remaining months of the start year.

	if ett[0] > stt[0] and stt[1] != 12:
	    for i in range(stt[1] + 1, 13):
		mstr = time.strftime("%B", (stt[0], i, 1, 0, 0, 0, 0, 0, 0))
		print
                print "Archiving %s %d..." % (mstr, stt[0])
		val = self.archive_days(stt[0], i, 1,
					calendar.monthrange(stt[0], i)[1],
					save_counts)
		count = count + val[0]
		err_n = err_n + val[1]

	# -- If the end date is more than a year away, retrieve the full
	#    years in-between.

	if ett[0] > stt[0] + 1:
	    for i in range(stt[0] + 1, ett[0]):
		for j in range(1, 13):
		    mstr = time.strftime("%B", (i, j, 1, 0, 0, 0, 0, 0, 0))
		    print
                    print "Archiving %s %d..." % (mstr, i)
		    val = self.archive_days(i, j, 1,
					    calendar.monthrange(i, j)[1],
					    save_counts)
		    count = count + val[0]
		    err_n = err_n + val[1]

	# -- If the end date is in the same year and month as the start
	#    month, we're good.

	if ett[0] == stt[0] and ett[1] == stt[1]:
	    pass
	else:

	    # -- If the end date is in the same year as the start date,
	    #    retrieve entries from the month after the start month, to
	    #    the month before the end month. Otherwise go from 1 to
	    #    the month before the end month.

	    if ett[0] > stt[0] or ett[1] > stt[1] + 1:
		if ett[0] == stt[0]:
		    s_month = stt[1] + 1
		else:
		    s_month = 1
		for i in range(s_month, ett[1]):
		    mstr = time.strftime("%B",
					 (ett[0], i, 1, 0, 0, 0, 0, 0, 0))
		    print
                    print "Archiving %s %d..." % (mstr, ett[0])
		    val = self.archive_days(ett[0], i, 1,
					    calendar.monthrange(ett[0], i)[1],
					    save_counts)
		    count = count + val[0]
		    err_n = err_n + val[1]

	    # -- Get the end month itself.

	    mstr = time.strftime("%B", (ett[0], ett[1], 1, 0, 0, 0, 0, 0, 0))
	    print
	    print "Archiving up until %s %d, %d..." % (mstr, ett[2], ett[0])
	    val = self.archive_days(ett[0], ett[1], 1, ett[2], save_counts)
	    count = count + val[0]
	    err_n = err_n + val[1]

	print
	if count > 1:
            print "Archived a total of %d posts." % (count),
	else:
	    print "Archived one post.",
	if err_n == 0:
	    print "No errors."
	elif err_n == 1:
	    print "Failed to archive one post."
	else:
            print "Failed to archive %d posts." % (err_n)
	self.reset_metadata()


    def organize_menu(self):
	"Select organization method."

	print """
SELECT ARCHIVE ORGANIZATION

[n] Do not create subdirectories.
[y] Create a subdirectory for each year.
[m] Create a subdirectory for each month."""
	print
	res = self.get_choice()

	if res in ("N", "n"):
	    self.Params["organize"] = "none"
	elif res in ("Y", "y"):
	    self.Params["organize"] = "year"
	elif res in ("M", "m"):
	    self.Params["organize"] = "month"
	else:
	    print "That is not a valid option."


    def pick_archive_menu(self):
	"Select posts to archive."

	print
	print "SELECT POSTS TO ARCHIVE"
	print

	if self.Cache.Journals != []:
	    print "[j] Change journal to take posts from:",
	    try:
		print self.Params["usejournal"]
	    except KeyError:
		print "(" + self.Params["user"] + ")"

	print "[s] Select start date of posts to archive:",
	try:
	    print time.strftime("%Y-%m-%d", self.Params["start_ttup"])
	except KeyError:
	    print "(none)"

	print "[e] Select end date of posts to archive:",
	try:
	    print time.strftime("%Y-%m-%d", self.Params["end_ttup"])
	except KeyError:
	    print "(today)"

	print "[d] Change archive directory: " + self.Params["archive_dir"]
	print "[o] Change archive organization: " + self.Params["organize"]
	print "[a] Run archive."

	print "[r] Return to main menu."
	print "[q] Quit."
	print
	res = self.get_choice()

	if res in ("R", "r"):
	    return 0
	elif res in ("Q", "q"):
	    self.do_quit()
	elif res in ("A", "a"):
	    self.mass_archive()
	elif res in ("J", "j") and self.Cache.Journals != []:
	    self.journal_menu()
	elif res in ("O", "o"):
	    self.organize_menu()
	elif res in ("S", "s"):
	    try:
		self.Params["start_ttup"] = self.select_date()
	    except ValueError:
		pass
	elif res in ("E", "e"):
	    try:
		self.Params["end_ttup"] = self.select_date()
	    except ValueError:
		pass
	elif res in ("D", "d"):
	    print
	    res = sane_raw_input("Directory to use as base of archive: ")
	    print
	    adir = os.path.expanduser(res)
	    ok = create_dir(adir, "archive")
	    if ok == 0:
		print "Archive directory unchanged."
	    else:
		self.Params["archive_dir"] = adir
	else:
	    print "That is not a valid option."
	return 1


   # -----
   # Main menu.
   # -----

    def main_menu(self):
	"Main menu."

	print
	print "MAIN MENU"
	print
	if self.LoggedIn == 0:
	    print "[l] Log in as " + self.Params["user"] + "."
	print """\
[p] Post a journal entry.
[r] Resume working on a previous draft.
[e] Edit or delete a posted journal entry.
[a] Archive past journal entries.
[x] Execute an administrative console command.
[v] View your current journal page.
[c] Check if your friends have posted updates.
[i] Information about the Charm client.
[q] Quit."""
	print
	res = self.get_choice()

	if self.LoggedIn == 0 and res in ("L", "l"):
	    print barline
	    print
	    self.cli_login()
	    print
	    print barline
	elif res in ("C", "c"):
	    print
	    self.prepare_checkfriends()
	    self.cli_checkfriends()
	    print
	elif res in ("P", "p"):
	    repeat_ok = 1
	    while repeat_ok:
		try:
		    repeat_ok = self.post_menu()
		except KeyboardInterrupt:
		    self.handle_interrupt("posting")
	elif res in ("R", "r"):
	    self.drafts_menu()
	elif res in ("E", "e"):
	    self.save_session()
	    self.clear_metadata()
	    repeat_ok = 1
	    while repeat_ok:
		try:
		    repeat_ok = self.pick_edit_menu()
		except KeyboardInterrupt:
		    self.handle_interrupt("selection")
	elif res in ("A", "a"):
	    self.save_session()
	    self.clear_metadata()
	    old_archive_dir = self.Params["archive_dir"]
	    old_archive_subdirs = self.Params["archive_subdirs"]
	    old_organize = self.Params["organize"]
	    self.Params["archive_subdirs"] = "0"
	    repeat_ok = 1
	    while repeat_ok:
		try:
		    repeat_ok = self.pick_archive_menu()
		except KeyboardInterrupt:
		    self.handle_interrupt("selection")
	    self.Params["archive_dir"] = old_archive_dir
	    self.Params["archive_subdirs"] = old_archive_subdirs
	    self.Params["organize"] = old_organize
        elif res in ("X", "x"):
            repeat_ok = 1
            while repeat_ok:
                try:
                    repeat_ok = self.console_menu()
                except KeyboardInterrupt:
                    self.handle_interrupt("selection")
	elif res in ("Z", "z"):
	    self.dump_debug_info()
	elif res in ("Q", "q"):
	    self.do_quit()
	elif res in ("I", "i"):
	    client_info()
	elif res in ("V", "v"):
	    try:
		import webbrowser
		webbrowser.open("http://www.livejournal.com/users/" +
				self.Params["user"])
	    except ImportError:
		print "Sorry. Your Python installation lacks the required webbrowser module."
	else:
	    print "That is not a valid option."

# ----------------------------------------------------------------------------
# Login utility: Username/password determination.
# ----------------------------------------------------------------------------

    def get_userpass(self, def_user = ""):
	"Figure out what username and password we're using."

	# -- If we got no conf directives at all, prompt the user.

	if self.Logins == {}:
	    print
	    ustr = sane_raw_input("LiveJournal Username: ")
	    print
	    ustr = string.strip(ustr)
	    if ustr == "":
		print "You need to enter a LiveJournal username." 
		return 0
	    self.Params["user"] = ustr

            try:
                import getpass
                ustr = getpass.getpass("LiveJournal Password: ")
            except:
                ustr = sane_raw_input("LiveJournal Password: ")
	    print
	    ustr = string.strip(ustr)
	    if ustr == "":
		print "You need to enter a LiveJournal password."
		return 0
	    self.Params["hpassword"] = md5digest(ustr)
	    print

	    self.Logins[self.Params["user"]] = self.Params["hpassword"]
	    return 1

	# -- If we had a default user specified, use that.

	if def_user != "":
	    try:
		self.Params["user"] = def_user
		self.Params["hpassword"] = self.Logins[def_user]
	    except KeyError:
		print "No login or hlogin parameter specified for default user %s." % (def_user)
		return 0
	    return 1

	# -- If we only have one available option, use it.

	if len(self.Logins) == 1:
	    self.Params["user"] = self.Logins.keys()[0]
	    self.Params["hpassword"] = self.Logins[self.Logins.keys()[0]]
	    return 1

	# -- We have no default. Prompt for one.

	ok = self.username_menu()
	if ok == 0:
	    return 0
	return 1

# ----------------------------------------------------------------------------
# Handling checkfriends-only mode.
# ----------------------------------------------------------------------------

    def checkfriends_mode(self, retry_secs):
	"Run a checkfriends, standalone or in loop mode, only."

	# -- If we've been given any groups to check, our task is more
	#    complex; we need to obtain the friend groups first. Note
	#    that we could have gotten the groups from the conf file,
	#    not just on the command line. Command-line choices completely
	#    override configuration defaults.

	self.prepare_checkfriends()

	if retry_secs == 0 and self.CheckDelay == 0:
	    self.cli_checkfriends()
	else:
	    if self.CheckDelay == 0:
		self.CheckDelay = retry_secs
	    self.checkfriends_loop()


# ----------------------------------------------------------------------------
# Handling quick mode.
# ----------------------------------------------------------------------------

    def quick_mode(self, draft_name = "", retry_secs = 15):
	"Post the text from stdin."

	if draft_name == "":
	    self.read_event(sys.stdin)
	else:
	    ok = self.slurp_draft("Post")
	    if ok == 0:
		return

	if self.Params["event"] == "":
	    print "Error: Empty message body. No post made."
	    return

	if self.Params.has_key("year") == 0:
	    ttup = time.localtime(time.time())
	    self.populate_time(ttup)

	ok = 0
	while ok == 0:
	    ok = self.cli_postevent()
	    if ok == 0:
                print "Waiting %d seconds to retry..." % (retry_secs)
		time.sleep(retry_secs)

	if self.getval("archive", "0") == "1":
	    if draft_name == "":
		self.make_archive(0, self.Params["event"])
	    else:
		self.make_archive(0)

	print "Posted."
	

# ----------------------------------------------------------------------------
# Post-login command-line options.
# ----------------------------------------------------------------------------

    def set_bool_opt(self, kname, sval):
	"Handle boolean command-line options."

	if sval == "":
	    x = 1
	else:
	    x = parse_bool(string.lower(sval))

	if x == -1:
	    print "Invalid value for command-line option %s: %s" % (kname, sval)
	else:
	    if Bool_Opts[kname] == 1:
		if x == 0:
		    x = 1
		else:
		    x = 0
	    if x == 1:
		self.Params[Bool_Map[kname]] = "1"
	    else:
		try:
		    del self.Params[Bool_Map[kname]]
		except:
		    pass


    def set_cmd_opts(self, opts):
	"Handle second half of command-line options."

	for o, a in opts:
	    if o in ("-d", "--drafts"):
		self.Params["draft_dir"] = os.path.expanduser(a)
	    elif o in ("-a", "--archive"):
		self.Params["archive_dir"] = os.path.expanduser(a)
	    elif o in ("-s", "--subject"):
		self.Params["subject"] = a[:255] # truncate to max length    
	    elif o in ("-m", "--mood"):
		self.set_mood(a)
	    elif o in ("-k", "--pic"):
		if a != "":
		    self.set_pickw(a)
	    elif o in ("-p", "--permit", "--security"):
		try:
		    self.set_security(a)
		except ValueError:
		    pass
	    elif o in ("-j", "--journal"):
		self.Params["usejournal"] = string.lower(a)
	    elif o in ("-M", "--music"):
		self.Params["prop_current_music"] = a
            elif o in ("-A", "--autodetect"):
                self.autodetect_music()
	    elif o in ("--autoformat", "--backdate", "--comments",
		       "--noemail"):
		self.set_bool_opt(o[2:], a)

# ----------------------------------------------------------------------------
# Main body of execution.
# ----------------------------------------------------------------------------

def usage():
    print "\nUsage: %s [options]" % (sys.argv[0])
    print """
Options:

  -h, --help               Print this usage message.
  -o, --options            Print additional posting options.

  -f, --file FILENAME      Use this file as your .charmrc
  -u, --user USERNAME      Use this as your initial username.
  -l, --login              Log in automatically.
  -n, --nologin            Don't log in automatically.
  -d, --drafts DIR         Save drafts in this directory.
  -a, --archive DIR        Archive old posts in this directory.
  -r, --resume DRAFTFILE   Resume working on a previous draft.
  -q, --quick              Quick posting mode.
  -c, --check              Check for friend updates only.
  -i, --interval MINUTES   Check for friend updates this often.
  -g, --group FRIENDGROUP  Check only this friend group for updates. You
                           can specify this option multiple times.
"""


def postopt_usage():
    print "\nUsage: %s [options]" % (sys.argv[0])
    print """
Options:

  -h, --help               Print main usage message and command-line options.
  -o, --options            Print these additional posting options. They all
                           specify an attribute for your next post.

  -s, --subject "SUBJECT"  Specify subject.
  -p, --permit PERMISSION  Specify security permission: friends, private, etc.
  -m, --mood MOOD          Specify mood.
  -k, --pic KEYWORD        Specify picture keyword.
  -M, --music "MUSIC"      Specify music.
  -A, --autodetect         Autodetect music using XMMS.
  --autoformat=[on|off]    Format your post yourself? Set this to off.
  --backdate=[on|off]      Backdate your post?
  --comments=[on|off]      Allow others to comment on this post?
  --noemail=[on|off]       Don't want comments emailed to you? Set this to off.
"""


def main():

    short_opts = "hoAf:u:clnqs:m:k:p:M:j:d:a:r:i:g:"
    long_opts = [ "help", "options", "autodetect",
		  "file=", "user=", "check", "login", "nologin",
		  "quick", "subject=", "mood=", "pic=", "music=",
		  "journal=", "permit=", "security=",
		  "drafts=", "archive=", "resume=", "interval=", "group=",
		  "autoformat=", "backdate=", "comments=", "noemail=" ]

    try:
	opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
    except getopt.GetoptError:
	usage()
	sys.exit(1)

    try:
	user_home_dir = os.environ['HOME']
    except KeyError:
	user_home_dir = "/tmp"
    rc_file = user_home_dir + "/" + ".charmrc"

    # -- Process the options we need to know about before a login attempt.

    quick_opt = 0
    login_opt = -1
    ckfronly = 0
    ckfrdelay = 0
    resumeold = ""
    def_user = ""
    ckfrgroups = []

    for o, a in opts:
	if o in ("-h", "--help"):
	    usage()
	    sys.exit(0)
	elif o in ("-o", "--options"):
	    postopt_usage()
	    sys.exit(0)
	elif o in ("-f", "--file"):
	    rc_file = os.path.expanduser(a)
	elif o in ("-u", "--user"):
	    def_user = a
	elif o in ("-l", "--login"):
	    login_opt = 1
	elif o in ("-n", "--nologin"):
	    login_opt = 0
	elif o in ("-c", "--check"):
	    ckfronly = 1
	elif o in ("-q", "--quick"):
	    quick_opt = 1 
	elif o in ("-i", "--interval"):
	    try:
		ckfrdelay = int(a) * 60
		if ckfrdelay < 60:
		    ckfrdelay = 0
	    except ValueError:
		pass
	elif o in ("-g", "--group"):
	    ckfrgroups.append(string.lower(a))
	elif o in ("-r", "--resume"):
	    resumeold = a

    jobj = Jabber()
    jobj.read_rcfile(rc_file);

    # -- Figure out what login ID we're using.

    if def_user != "":
	ok = jobj.get_userpass(def_user)
    elif jobj.Params.has_key("default_user"):
	ok = jobj.get_userpass(jobj.Params["default_user"])
    else:
	ok = jobj.get_userpass()

    if ok == 0:
	sys.exit(1)

    # -- Load cache if we can, and if we need to.

    if ckfronly == 0 or ckfrgroups != []:
	jobj.Cache.load_cache("%s/.charmcache" % (user_home_dir),
			      jobj.Params["user"])

    # -- Quick friends check. Implies we do nothing else.

    if ckfrgroups != []:
	jobj.CheckGroups = ckfrgroups

    if ckfronly == 1:
	jobj.checkfriends_mode(ckfrdelay)
	sys.exit(0)

    # -- Save off conf options we'll want to preserve across operations.

    for k in Conf_MetaData:
	try:
	    jobj.Save_Meta[k] = jobj.Params[k]
	except KeyError:
	    pass

    # -- Welcome banner and login.
    #    We default to logging in. Command-line options override anything
    #    in the conf file. Otherwise, we check for the nologin conf option;
    #    if that's on, don't log in.

    if login_opt == -1:
	if jobj.getval("nologin", "0") == "1":
	    login_opt = 0
	else:
	    login_opt = 1

    if quick_opt == 0:
	if login_opt == 1:
	    print "( Logging in... )"
	print "\n---------------------[ Charm " + __version__ + ", a LiveJournal Client ]--------------------\n"

    if login_opt == 1:
	jobj.cli_login(quick_opt)
	if quick_opt == 0:
	    print
	    print barline

    # -- We have to handle security after logging in, so we can deal
    #    with friends groups.

    try:
	jobj.set_security(jobj.Params["security"])
    except KeyError:
	pass
    except ValueError:
	print "Warning: invalid value for security option."

    # -- Process the options we should look at after startup.
    #    This means such options override the rc file.
    #
    #    The first thing we have to do, though, is to look to see if we
    #    got a resume directive, because we need to load that up first,
    #    then allow other options to override it.

    if resumeold != "":
	jobj.resume_draft(resumeold, 1)

    jobj.set_cmd_opts(opts)

    # -- If we're in quick mode, we grab the post from stdin and just
    #    send it to the server.

    if quick_opt == 1:
	jobj.quick_mode(resumeold)
	sys.exit(0)

    # -- Loop main menu infinitely.

    while 1:
	try:
	    jobj.main_menu()
	except KeyboardInterrupt:
	    jobj.handle_interrupt("main")
