#!/usr/local/bin/python -OQnew


import sys
import os
import select
import string
import traceback
import urllib
import time

import msnlib
import msncb

"""
MSN Client

This is an example (yet usable) msn client.
"""

#
# colors, for nice output
#
class color:
	def __init__(self):
		self.black = 	'\x1b[0;30m'
		self.red =	'\x1b[0;31m'
		self.green =	'\x1b[0;32m'
		self.yellow =	'\x1b[0;33m'
		self.blue =	'\x1b[0;34m'
		self.magenta =	'\x1b[0;35m'
		self.cyan =	'\x1b[0;36m'
		self.white =	'\x1b[0;37m'
		self.normal =	'\x1b[0m'
		self.bold =	'\x1b[1m'
		self.clear =	'\x1b[J'

c = color()


#
# different useful prints
#

def printl(line, color = c.normal, bold = 0):
	"Prints a line with a color"
	out = ''
	if bold:
		out = c.bold
	out = color + out + line + c.normal
	sys.stdout.write(out)
	sys.stdout.flush()
	

def perror(line):
	"Prints an error"
	out = ''
	out += c.yellow + c.bold + '!' + c.normal
	out += c.red + c.bold + '!' + c.normal
	out += c.blue + c.bold + '!' + c.normal
	out += ' ' + c.green + c.bold + line + c.normal + '\a'
	sys.stdout.write(out)
	sys.stdout.flush()

def pexc(line):
	"Prints an exception"
	out = '\n'
	out += ( c.cyan + c.bold + '!' + c.normal ) * 3
	sys.stdout.write(out)
	sys.stdout.write(c.bold + line)
	sys.stdout.flush()
	traceback.print_exc()
	sys.stdout.write(c.normal)
	sys.stdout.write('\n')
	sys.stdout.flush()

def print_list(md, only_online = '', userlist = None, include_emails = 0):
	"Prints the user list"
	if not userlist:
		userlist = md.users
	ul = userlist.keys()
	ul.sort()
	for email in ul:
		u = userlist[email]
		if u.status != 'FLN': 
			sys.stdout.write(c.bold)
		else:
			if only_online:		# this is not nice, but it's simple enough
				continue
		status = msnlib.reverse_status[u.status]
		print '%7.7s :: %s' % (status, u.nick),
		if include_emails: print '(%s)' % (email),
		print c.normal

def print_user_info(email):
	"Prints the user information, and pending messages"
	u = m.users[email]
	out = c.bold
	out += c.bold + 'User info for ' + email + '\n'
	out += c.bold + 'Nick:\t\t' + c.normal + u.nick + '\n'
	out += c.bold + 'Status:\t\t' + c.normal + msnlib.reverse_status[u.status] + '\n'
	if u.homep: out += c.bold + 'Home phone:\t' + c.normal + u.homep + '\n'
	if u.workp: out += c.bold + 'Work phone:\t' + c.normal + u.workp + '\n'
	if u.mobilep: out += c.bold + 'Mobile phone:\t' + c.normal + u.mobilep + '\n'
	if u.sbd: 
		out += c.bold + 'Switchboard:\t' + c.normal + str(u.sbd) + '\n'
		if u.sbd.msgqueue:
			out += c.bold + 'Pending messages:' + '\n'
			for msg in u.sbd.msgqueue:
				out += c.bold + '\t>>> ' + c.normal + msg + '\n'
	printl(out)
	
def print_prompt():
	"Prints the user prompt"
	sys.stdout.write('\r' + c.red + c.bold + '[msn] ' + c.normal)
	sys.stdout.flush()

def print_inc_msg(email, lines, eoh = 0, quiet = 0, ptime = 1, recvtime = 0):
	"""Prints an incoming message from a list of lines and an optional
	end-of-header pointer.  You can also pass the original received time as
	a parameter, this is used for history printed."""
	nick = email2nick(email)
	if not nick: nick = email
	if ptime:
		if recvtime:
			ctime = time.strftime('%I:%M:%S%p', time.localtime(recvtime))
		else:
			ctime = time.strftime('%I:%M:%S%p')
		printl('%s ' % ctime, c.blue)
	printl('%s' % nick, c.cyan, 1)
	if len(lines[eoh:]) == 1:
		printl(' <<< %s\n' % lines[eoh], c.yellow, 1)
	else:
		printl(' <<< \n\t', c.yellow, 1)
		msg = string.join(lines[eoh:], '\n\t')
		msg = msg.replace('\r', '')
		printl(msg + '\n', c.bold)
	beep(quiet)

def print_out_msg(nick, msg):
	"Prints an outgoing message"
	ctime = time.strftime('%I:%M:%S%p')
	printl('%s ' % ctime, c.blue)
	printl('%s' % nick, c.cyan, 1)
	printl(' >>> ', c.yellow, 1)
	printl('%s\n' % msg)


def beep(q = 0):
	"Beeps unless it's told to be quiet"
	if not q:
		printl('\a')
	
	
#
# useful functions
#

def nick2email(nick):
	"Returns an email according to the given nick, or None if noone	matches"
	for email in m.users.keys():
		if m.users[email].nick == nick:
			return email
	return None

def email2nick(email):
	"Returns a nick accoriding to the given email, or None if noone matches"
	if email in m.users.keys():
		return m.users[email].nick
	else:
		return None

def get_config(file):
	"Parses a simple config file, and returns a var:value dict"
	try:
		fd = open(file)
	except:
		return None
	lines = fd.readlines()
	config = {}
	for i in lines:
		i = i.strip()
		if i.find('=') < 0:
			continue
		if i[0] == '#':
			continue
		var, value = i.split('=', 1)
		var = var.strip()
		value = value.strip()
		config[var] = value
	return config

def null(s):
	"Null function, useful to void debug ones"
	pass
		
def log_msg(email, type, msg, mtime = 0):
	file = config['history directory'] + '/' + email
	if not mtime:
		mtime = time.time()
	out = ''
	out += time.strftime('%d/%b/%Y %H:%M:%S ', time.localtime(mtime))
	out += email + ' '
	if type == 'in':
		out += '<<< '
		msg = msg.replace('\r', '')
		lines = msg.split('\n')
		if len(lines) == 1:
			 out += msg + '\n'
		else:
			out += '\n\t'
			out += string.join(lines[:], '\n\t')
			out += '\n'
	elif type == 'out':
		out += '>>> ' + msg + '\n'
	elif type == 'status':
		out += '*** ' + msg + '\n'
	
	fd = open(file, 'a')
	fd.write(out)
	fd.close()
	del(fd)
	return 1


#
# stdin command parser
#

def parse_cmd(cmd):
	global c, last_sent, last_received
	# only newline or empty string
	if len(cmd) <= 1:
		if len(cmd) == 0:
			print
			print '!!! EOF'
			m.disconnect()
			sys.exit(0)
		else:
			return ''
			
	# cut trailing newline
	if cmd[-1] == '\n':
		cmd = cmd[:-1]

	cmd = cmd.split()
	if len(cmd) > 1:
		cmd, params = cmd[0], string.join(cmd[1:])
	else:
		cmd = cmd[0]
		params = ''
	
	# parse
	if   cmd == 'status': 		# change status
		if not params:
			return 'Status: %s' % msnlib.reverse_status[m.status]
		if not m.change_status(params):
			return 'Status must be one of: online, away, busy, brb, phone, lunch, invisible, idle or offline'
		return 'Status changed to: %s' % params
		
	elif cmd == 'q':		# quit
		printl('Closing\n', c.green, 1)
		m.disconnect()
		sys.exit(0)
	
	elif cmd == 'reload':		# reload callbacks
		reload(msncb)
		m.cb = msncb.cb()
	
	elif cmd == 'w':		# list
		print_list(m)
	
	elif cmd == 'e':		# list (online only)
		print_list(m, 1)
	
	elif cmd == 'wr':		# reverse list
		print_list(m, userlist = m.reverse, include_emails = 1)

	elif cmd == 'raw':		# send a raw message
		try:
			c = params[0:3]
			p = params[4:]
		except:
			return 'Error parsing cmd'
		m._send(c, p)
	
	elif cmd == 'debug':		# enable/disable debugging
		p = params.split()
		if len(p) != 1:
			return 'Error parsing cmd'
		if p[0] == 'off':
			msnlib.debug = null
			msncb.debug = null
			return 'Debugging disabled'
		elif p[0] == 'on':
			reload(msnlib)
			reload(msncb)
			return 'Debugging enabled'
		else:
			return 'Unknown parameter - must be "on" or "off"'
	
	elif cmd == 'config':		# show config variables
		keys = config.keys()
		keys.sort()
		for var in keys:
			value = str(config[var])
			if var == 'password':
				value = '<not displayed>'
			printl(c.bold + var + ' = ' + c.normal + value + '\n')
	
	elif cmd == 'close':		# close a connection
		p = params.split()
		if len(p) != 1: 
			return 'Error parsing cmd'
		email = nick2email(p[0])
		if not email: 
			return 'Unknown nick (%s)' % str(email)
		if not m.users[email].sbd:
			return 'No socket opened for %s' % nick
		desc = str(m.users[email].sbd)
		m.close(m.users[email].sbd)
		return 'Closed socket %s' % desc
	
	elif cmd == 'privacy':		# set privacy mode
		p = params.split()
		if len(p) != 2: return 'Error parsing cmd'
		m.privacy(int(p[0]), int(p[1]))
	
	elif cmd == 'add':		# add a user
		p = params.split()
		if   len(p) == 0: 
			return 'Error parsing cmd'
		elif len(p) == 1: 
			email = nick = p[0]
		else: 
			email = p[0]
			nick = p[1]
		m.useradd(email, nick)
	
	elif cmd == 'del':		# delete a user
		p = params.split()
		if len(p) != 1: return 'Error parsing cmd'
		email = nick2email(p[0])
		m.userdel(email)
	
	elif cmd == 'nick':		# change our nick
		p = params.split()
		if len(p) != 1: return 'Error parsing cmd'
		nick = p[0]
		m.change_nick(nick)
	
	elif cmd == 'info':		# user info
		p = params.split()
		if len(p) != 1:
			out = ''
			out += c.bold + 'Info for ' + m.email + '\n'
			out += c.bold + 'Nick:\t\t' + c.normal + m.nick + '\n'
			out += c.bold + 'Status:\t\t' + c.normal + msnlib.reverse_status[m.status] + '\n'
			out += c.bold + 'Home phone:\t' + c.normal + str(m.homep) + '\n'
			out += c.bold + 'Work phone:\t' + c.normal + str(m.workp) + '\n'
			out += c.bold + 'Mobile phone:\t' + c.normal + str(m.mobilep) + '\n'
			out += c.bold + 'Users in contact list: ' + str(len(m.users)) + '\n'
			out += c.bold + 'Users in reverse list: ' + str(len(m.reverse)) + '\n'
			out += c.bold + 'Notification server: ' + c.normal + str(m) + '\n'
			if m.sb_fds:
				out += c.bold + 'Switchboard connections:\n'
				for i in m.sb_fds:
					out += c.bold + '\tSB: ' + c.normal + str(i) + '\n'
			printl(out)
		else:
			email = nick2email(p[0])
			if not email:
				return 'Unknown nick (%s)' % str(email)
			print_user_info(email)
	
	elif cmd == 'sync':		# manual sync
		m.sync()
	
	elif cmd == 'h':		# show history
		printl('Incoming Message History (last %d messages)\n' % config['history size'], c.green, 1)
		for i in history_ring:
			rtime = i[0]
			email = i[1]
			msg = i[2]
			print_inc_msg(email, msg, quiet = 1, ptime = 1, recvtime = rtime)

	# send a message
	elif cmd == 'm' or cmd == 'msg' or cmd == 'r' or cmd == 'a':
		if cmd == 'm' or cmd == 'msg':
			p = params.split()
			nick = p[0]
			email = nick2email(nick)
			msg = string.join(p[1:])
		elif cmd == 'r':
			email = last_received
			nick = email2nick(email)
			if not nick: nick = email
			msg = params
		elif cmd == 'a':
			email = last_sent
			nick = email2nick(email)
			if not nick: nick = email
			msg = params
		if not email:
			return 'Unknown nick (%s)' % str(email)
		r = m.sendmsg(email, msg)
		last_sent = email
		if r == 1:
			return 'Message for %s queued for delivery' % nick
		elif r == 2:
			print_out_msg(nick, msg)
			log_msg(email, 'out', msg)
		elif r == -2:
			return 'Message too big'
		else:
			return 'Error %d sending message' % r
		
	elif cmd == 'help' or cmd == '?':
		r = ''
		r += 'Command list:\n'
		r += 'status [mode]\t Changes status to "mode", which can be '
		r += 'one of: online, away, busy, brb, phone, lunch, '
		r += 'invisible, idle or offline; or show the current status\n'
		r += 'q\t\t Quits the program\n'
		r += 'w\t\t Prints your entire contact list\n'
		r += 'e\t\t Prints your online contacts\n'
		r += 'wr\t\t Prints your reverse contact list\n'
		r += 'h\t\t Shows your incoming message history\n'
		r += 'add email nick\t Adds the user "email" with the nick "nick"\n'
		r += 'del nick\t Deletes the user with nick "nick"\n'
		r += 'info [nick]\t Shows the user information and pending messages (if any), or our personal info\n'
		r += 'close nick\t Closes the switchboard connection with "nick"\n'
		r += 'config\t\t Shows the configuration\n'
		r += 'nick newnick\t Changes your nick to "newnick"\n'
		r += 'm nick text\t Sends a message to "nick" with the "text"\n'
		r += 'a text\t Sends a message with "text" to the last person you sent a message to\n'
		r += 'r text\t Sends a message with "text" to the last person that sent you a message\n'
		return r
	else:
		return 'Unk cmd'
	
	return ''



#
# This are the callback replacements, which only handle the output and then
# call the original callbacks to do the lower level stuff
#

# basic classes
m = msnlib.msnd()
m.cb = msncb.cb()

# status change
def cb_iln(md, type, tid, params):
	t = params.split()
	status = msnlib.reverse_status[t[0]]
	email = t[1]
	nick = md.users[email].nick
	printl('\n' + nick, c.blue, 1)
	printl(' changed status to ', c.magenta)
	printl('%s\n' % status, c.magenta, 1)
	log_msg(email, 'status', status)
	msncb.cb_iln(md, type, tid, params)
m.cb.iln = cb_iln

def cb_nln(md, type, tid, params):
	status = msnlib.reverse_status[tid]
	t = string.split(params)
	email = t[0]
	nick = md.users[email].nick
	printl('\n' + nick, c.blue, 1)
	printl(' changed status to ', c.magenta)
	printl('%s\n' % status, c.magenta, 1)
	log_msg(email, 'status', status)
	msncb.cb_nln(md, type, tid, params)
m.cb.nln = cb_nln

def cb_fln(md, type, tid, params):
	email = tid
	nick = md.users[email].nick
	printl('\n' + nick, c.blue, 1)
	printl(' disconnected\n', c.magenta)
	log_msg(email, 'status', 'disconnect')
	msncb.cb_fln(md, type, tid, params)
m.cb.fln = cb_fln

# server disconnect
def cb_out(md, type, tid, params):
	printl('\nServer sent disconnect', c.red)
	msncb.cb_out(md, type, tid, params)
m.cb.out = cb_out


# message
def cb_msg(md, type, tid, params, sbd):
	global last_received
	t = string.split(tid)
	email = t[0]
	# messages from hotmail are only when we connect, and send things
	# regarding, aparently, hotmail issues. we ignore them
	if email == 'Hotmail':
		return

	# parse
	lines = string.split(params, '\n')
	headers = {}
	eoh = 0
	for i in lines:
		# end of headers
		if i == '\r':
			break
		tv = string.split(i, ':')
		type = tv[0]
		value = string.join(tv[1:], ':')
		value = string.strip(value)
		headers[type] = value
		eoh += 1
	
	eoh +=1
	if headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-msmsgscontrol':
		nick = email2nick(email)
		if not nick: nick = email
		printl('\n')
		ctime = time.strftime('%I:%M:%S%p')
		printl('%s ' % ctime, c.blue)
		printl('%s' % nick, c.cyan, 1)
		printl(' is typing\n', c.magenta)
	else:
		print
		print_inc_msg(email, lines, eoh)
		log_msg(email, 'in', string.join(lines[eoh:], '\n'))

		# append the message to the list, keeping it below the configured limit
		if len(history_ring) > config['history size']: 
			del(history_ring[0])
		history_ring.append((time.time(), email, lines[eoh:]))
	
	last_received = email
	msncb.cb_msg(md, type, tid, params, sbd)
m.cb.msg = cb_msg


# join a conversation and send pending messages
def cb_joi(md, type, tid, params, sbd):
	email = tid
	if len(sbd.msgqueue) > 0:
		nick = email2nick(email)
		if not nick: nick = email
		printl('\nFlushing messages for %s:\n' % nick, c.green, 1)
		for msg in sbd.msgqueue:
			print_out_msg(nick, msg)
			log_msg(email, 'out', msg)
	msncb.cb_joi(md, type, tid, params, sbd)
m.cb.joi = cb_joi

# server errors
def cb_err(md, errno, params):
	if not msncb.error_table.has_key(errno):
		desc = 'Unknown'
	else:
		desc = msncb.error_table[errno]
	desc = 'Server sent error %d: %s\n' % (errno, desc)
	perror(desc)
	msncb.cb_err(md, errno, params)
m.cb.err = cb_err
	
# users add, delete and modify
def cb_add(md, type, tid, params):
	t = params.split()
	type = t[0]
	if type == 'RL' or type == 'FL':
		email = t[2]
		nick = urllib.unquote(t[3])
	if type == 'RL':
		out = c.blue + c.bold + ('\n%s (%s) ' % (email, nick)) + c.magenta + 'has added you\n'
		printl(out)
		beep()
	elif type == 'FL':
		out = c.blue + c.bold + ('\n%s (%s) ' % (email, nick)) + c.magenta + 'has been added to your contact list\n'
		printl(out)
	msncb.cb_add(md, type, tid, params)
m.cb.add = cb_add

def cb_rem(md, type, tid, params):
	t = params.split()
	type = t[0]
	if type == 'RL' or type == 'FL':
		email = t[2]
	if type == 'RL':
		out = '\n' + c.blue + c.bold + email + ' ' + c.magenta + 'has removed you\n'
		printl(out)
		beep()
	elif type == 'FL':
		out = '\n' + c.blue + c.bold + email + ' ' + c.magenta + 'has been removed from your contact list\n'
		printl(out)
	msncb.cb_rem(md, type, tid, params)
m.cb.rem = cb_rem



#
# now the real thing
#
printl('Starting up MSN Client C1\n', c.yellow, 1)

# first, the configuration
printl('Loading config... ', c.green, 1)
if len(sys.argv) > 1:
	file = sys.argv[1]
else:
	file = os.environ['HOME'] + '/.msn/msnrc'

config = get_config(file)
if not config:
	perror('Error opening config file (%s), try running "msnsetup"\n' % file)
	sys.exit(1)

# set the mandatory values
if config.has_key('email'): 
	m.email = config['email']
else: 
	perror('Error: email not specified in config file\n')
	sys.exit(1)

if config.has_key('password'):
	m.pwd = config['password']
else:
	perror('Error: password not specified in config file\n')
	sys.exit(1)

# and the optional ones, setting the defaults if not present
# history size
if not config.has_key('history size'): 
	config['history size'] = 10
else:
	try:
		config['history size'] = int(config['history size'])
	except:
		perror('history size must be integer, using default\n')
		config['history size'] = 10

# initial status
if not config.has_key('initial status'): 
	config['initial status'] = 'online'
elif config['initial status'] not in msnlib.status_table.keys():
	perror('unknown initial status, using default\n')
	config['initial status'] = 'online'

# debug
if not config.has_key('debug'):
	config['debug'] = 0
elif config['debug'] != 'yes':
	config['debug'] = 0

# log history
if not config.has_key('log history'):
	config['log history'] = 1
elif config['log history'] != 'yes':
	config['log history'] = 0

# history directory
if not config.has_key('history directory'):
	config['history directory'] = os.environ['HOME'] + '/.msn/history'

# auto away time
if not config.has_key('auto away'):
	config['auto away'] = 0
else:
	try:
		config['auto away'] = int(config['auto away'])
	except:
		perror('auto away must be integer, using default\n')
		config['auto away'] = 0
if config['auto away'] and config['auto away'] < 60:	# sanity check
	perror('Warning: auto away time was set to less than a minute!\n')

printl('done\n', c.green, 1)


# set or void the debug
if not config['debug']:
	msnlib.debug = null
	msncb.debug = null


# login to msn
printl('Logging in... ', c.green, 1)
try:
	m.login()
	printl('done\n', c.green, 1)
except 'AuthError', info:
	errno = int(info[0])
	if not msncb.error_table.has_key(errno):
		desc = 'Unknown'
	else:
		desc = msncb.error_table[errno]
	perror('Error: %s\n' % desc)
	sys.exit(1)
except:
	pexc('Exception logging in\n')
	sys.exit(1)


# call sync to get the lists and refresh
printl('Syncing... ', c.green, 1)
if m.sync():
	printl('done\n', c.green, 1)
else:
	perror('Error syncing users\n')

if m.change_status(config['initial status']):
	printl('Status set to %s\n' % config['initial status'], c.green, 1)
else:
	perror('Error setting status: unknown status %s\n' % config['initial status'])


# global variables
history_ring = []	# history buffer
last_sent = ''		# email of the last person we sent a message to
last_received = ''	# email of the last person we received a message from

# auto-away
timeout = config['auto away']
if not timeout:
	timeout = None		# must be None, not 0 because of select() semantics
auto_away = 0


beep()

# loop
print_prompt()
while 1:
	fds = m.pollable()
	infd = fds[0]
	outfd = fds[1]
	infd.append(sys.stdin)
	print_prompt()
	fds = select.select(infd, outfd, [], timeout)
	
	if timeout and len(fds[0] + fds[1]) == 0:
		# timeout, set auto away
		if m.status == 'NLN':
			m.change_status('away')
			auto_away = 1
			printl('\nAutomatically changing status to away\n', c.green, 1)
	
	for i in fds[0] + fds[1]:		# see msnlib.msnd.pollable.__doc__
		if i == sys.stdin:
			# auto away revival
			if auto_away:
				auto_away = 0
				m.change_status('online')
				printl('\nAutomatically changing status back to online\n', c.green, 1)
			# parse the output
			out = parse_cmd(i.readline())
			printl(out + '\n', c.green, 1)
		else:
			try:
				m.read(i)
			except 'SocketError':
				if i != m:
					#printl('\n!!! Socket %s closed\n' % i, c.green)
					if i.msgqueue:
						nick = email2nick(i.emails[0])
						printl("\nConnection with %s closed - the following messages couldn't be sent:\n" % nick, c.green, 1)
						for msg in i.msgqueue:
							printl(c.bold + '\t>>> ' + c.normal + msg + '\n')
					m.close(i)
				else:
					printl('\nMain socket closed\n', c.red)
					sys.exit(1)



