#!/usr/bin/env python 


import sys
import os
import socket
import select
import string

import msnlib
import msncb


"""
MSN Client Daemon

This is a MSN client that reads commands from a named pipe, using
a little text-only protocol. It's main use is to serve as a 'glue'
to implement clients in other languages.

This is yet experimental because lack of testing, please let me know if you
try it out.
"""


def null(s):
	"Null function, useful to void debug ones"
	pass
		

#
# 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]
	equeue.append('STCH %s %s\n' % (email, 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]
	equeue.append('STCH %s %s\n' % (email, status))
	msncb.cb_nln(md, type, tid, params)
m.cb.nln = cb_nln

def cb_fln(md, type, tid, params):
	email = tid
	u = m.users[email]
	discarded = 0
	if u.sbd and u.sbd.msgqueue:
		discarded = len(u.sbd.msgqueue)
	equeue.append('STCH %s offline %d\n' % (email, discarded))
	msncb.cb_fln(md, type, tid, params)
m.cb.fln = cb_fln

# server disconnect
def cb_out(md, type, tid, params):
	equeue.append('ERR SERV_DISC Server sent disconnect\n')
	msncb.cb_out(md, type, tid, params)
m.cb.out = cb_out


# message
def cb_msg(md, type, tid, params, sbd):
	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 (basically
	# because i couldn't care less; however if somebody has intrest in
	# these and provides some debug output i'll be happy to implement
	# parsing).
	if email == 'Hotmail':
		return

	# parse
	lines = string.split(params, '\n')
	headers = {}
	eoh = 1
	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
	
	if headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-msmsgscontrol':
		# the typing notices
		equeue.append('TYPING %s\n' % email)
	else:
		# messages
		equeue.append('MSG %d %d %s\n%s\n' % \
			(len(lines), eoh, email, string.join(lines, '\n')) )
	
	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:
		equeue.append('MFLUSH %s\n' % email)
	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]
	equeue.append('ERR %s %s\n' % (errno, 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]
	if type == 'RL':
		equeue.append('UADD %s\n' % email)
	elif type == 'FL':
		equeue.append('ADDFL %s\n' % email)
	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':
		equeue.append('UDEL %s\n' % email)
	elif type == 'FL':
		equeue.append('DELFL %s\n' % email)
	msncb.cb_rem(md, type, tid, params)
m.cb.rem = cb_rem


def login(email, password):
	# login to msn
	printl('Logging in... ', c.green, 1)
	try:
		m.login()
		printl('done\n', c.green, 1)
	except msnlib.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)
		quit(1)
	except KeyboardInterrupt:
		quit()
	except (msnlib.SocketError, socket.error), info:
		perror('Network error: ' + str(info) + '\n')
		quit(1)
	except:
		pexc('Exception logging in\n')
		quit(1)


#
# the pipe read
#

# first, a small send wrapper to avoid repeating 'addr' all over the place
# note that as they are implemented using udp, if more than one client
# connects it can get quite quite messy
addr = ()
def psend(pipe, s):
	print '-->', s,
	return pipe.sendto(s, addr)

# read from the pipe, c being the pipe socket passed from the caller
def pipe_read(c):
	global m
	global addr
	global equeue	
	
	# we don't worry about lines too much in this implementation because
	# we use datagrams. however, when using stream sockets you should
	s, addr = c.recvfrom(4 * 1024)	# input buffer, should be enough
	print '<--', s,
	try:
		s = s.split(' ', 1)
		if len(s) == 2:
			cmd, params = s
		else:
			cmd = s[0]
			params = ''

		cmd = cmd.strip()
		if params:
			params = params.strip()
			params = params.split(' ')
	except:
		psend(c, 'ERR EINVAL\n')
		return

	if cmd == 'LOGIN':
		if len(params) != 2:
			psend(c, 'ERR PARAMS\n')
			return
		try:
			email, pwd = params
			m.email = email
			m.pwd = pwd
			m.login()
			m.sync()
		except msnlib.AuthError, info:
			errno = int(info[0])
			if not msncb.error_table.has_key(errno):
				desc = 'Unknown'
			else:
				desc = msncb.error_table[errno]
			psend(c, 'ERR MSN %d %s\n' % (errno, desc))
			return
		except (msnlib.SocketError, socket.error), info:
			psend(c, 'ERR SOCK %s\n' % str(info))
			return
		psend(c, 'OK\n')
		return
		
	elif cmd == 'LOGOFF':
		m.disconnect()
		psend(c, 'OK\n')
		return
		
	# if we are not connected, the following commands are not available
	if not m.fd:
		psend(c, 'ERR ENOTCONN\n')
		return
	
	
	if cmd == 'STATUS':
		status = string.join(params, ' ')
		if not m.change_status(status):
			psend(c, 'ERR UNK STATUS\n')
		else:
			psend(c, 'OK\n')
		return
		
	if cmd == 'POLL':
		equeue.append('POLLEND\n')
		for evt in equeue:
			psend(c, evt)
		equeue = []
		return
	
	if cmd == 'GETCL':
		psend(c, 'CL %d\n' % len(m.users.keys()) )
		for email in m.users.keys():
			u = m.users[email]
			status = msnlib.reverse_status[u.status]
			psend(c, '%s %s %s\n' % (status, email, u.nick))
		return
	
	if cmd == 'GETRCL':
		psend(c, 'CL %d\n' % len(m.reverse.keys()) )
		for email in m.reverse.keys():
			u = m.reverse[email]
			status = msnlib.reverse_status[u.status]
			psend(c, '%s %s %s\n' % (status, email, u.nick))
		return
	
	if cmd == 'INFO':
		if len(params) != 1:
			psend(c, 'ERR PARAMS\n')
			return
		if not m.users.has_key(email):
			psend(c, 'ERR UNK USER\n')
		u = m.users[email]
		psend(c, 'email = %s\n' % email)
		psend(c, 'nick = %s\n' % u.nick)
		psend(c, 'homep = %s\n' % u.homep)
		psend(c, 'workp = %s\n' % u.workp)
		psend(c, 'mobilep = %s\n' % u.mobilep)
		psend(c, '\n')
		return
	
	if cmd == 'ADD':
		if len(params) != 2:
			psend(c, 'ERR PARAMS\n')
			return
		nick, email = params
		m.useradd(email, nick)
		psend(c, 'OK\n')
		return
	
	if cmd == 'DEL':
		if len(params) != 1:
			psend(c, 'ERR PARAMS\n')
			return
		m.userdel(params)
		psend(c, 'OK\n')
		return
	
	if cmd == 'NICK':
		if len(params) != 1:
			psend(c, 'ERR PARAMS\n')
			return
		m.change_nick(params)
		psend(c, 'OK\n')
		return
	
	if cmd == 'PRIV':
		if len(params) != 2:
			psend(c, 'ERR PARAMS\n')
			return
		try:
			public = int(p[0])
			auth = int(p[1])
			if public not in (0, 1) or auth not in (0, 1):
				raise
		except:
			psend(c, 'ERR EINVAL\n')
			return
		m.privacy(public, auth)
		psend(c, 'OK\n')
		return
	
	if cmd == 'SENDMSG':
		params = string.join(params, ' ')
		params = string.split(params, '\n', 2)
		params, msg = params
		params = string.split(params, ' ')
		if len(params) < 2:
			psend(c, 'ERR PARAMS\n')
			return
		lines = params[0]
		email = params[1]
		msg = msg
		m.sendmsg(email, msg)
		psend(c, 'OK\n')
		return
		
	# if we got here is because the command is unknown		
	psend(c, 'ERR UNK\n')
	return
	
		

#
# now the real thing
#

# void the debug
msnlib.debug = null
msncb.debug = null

# POLL event queue
# We implement it in a very, very efficient way: text =)
# Yes, it's actually a list, but just because .append() is readable
# and allow us to keep track of the number of pending events
equeue = []

# open the socket for local communication
# we use datagram sockets to avoid complex reads and writes for now, but the
# protocol is line-oriented and perfectly capable of working over a stream
# socket.
pipe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
pipe.bind(('127.0.0.1', 3030))


# loop, waiting for connections
while 1:
	infd = outfd = []
	# if we are connected, poll from msn
	if m.fd != None:
		t = m.pollable()
		infd = t[0]
		outfd = t[1]
	infd.append(pipe)

	fds = select.select(infd, outfd, [], 0)
	
	for i in fds[0] + fds[1]:	# see msnlib.msnd.pollable.__doc__
		if i == pipe:
			# read from the pipe
			pipe_read(pipe)
		else:
			try:
				m.read(i)
			except (msnlib.SocketError, socket.error), err:
				if i != m:
					# user closed a connection
					# note that messages can be
					# lost here
					equeue.append('SCLOSE USER %s %d\n' % (i.emails[0], len(i.msgqueue)) )
					m.close(i)
				else:
					# main socket closed
					# report
					equeue.append('SCLOSE MAIN\n')
					quit(1)


