# pymuseekd - Python tools for museekd
#
# Copyright (C) 2003-2004 Hyriand <hyriand@thegraveyard.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import socket
import struct
import random

try:
	from Crypto.Hash import SHA256
	from Crypto.Cipher import AES

	class Cipher:
		def __init__(self, key):
			self.ctx = AES.new(SHA256.new(key).digest())
		def decipher(self, value):
			return self.ctx.decrypt(value)
		def cipher(self, value):
			block = value
			while len(block) % 16:
				block += chr(random.randint(0, 255))
			return self.ctx.encrypt(block)
	sha256Block = SHA256.new
except ImportError:
	from mucipher import Cipher, sha256Block

class InvalidHostException(Exception):
	pass

class InvalidMessageException(Exception):
	pass

class UnknownMessageException(Exception):
	pass

# Extract message codes and classes from messages.py and add them to MSGTAB
import messages
MSGTAB = {}
for _message in dir(messages):
	message = getattr(messages, _message)
	if not hasattr(message, 'code'):
		continue
	MSGTAB[message.code] = message

class Driver:
	def __init__(self):
		self.socket = None
		self.connected = False
		self.loggedin = False
		self.password = None
		self.mask = None
		self.cipher = None
		self.sync_id = 0
	
	# Connect to museekd, host in the form of "/tmp/museekd.user" for unix sockets
	# or "somehostname:port" for TCP sockets. Mask is an event mask (see messages.py)
	def connect(self, host, password, mask = 0):
		self.password = password
		self.mask = mask
		if host[:1] == '/':
			# Connect to a unix socket
			self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
			try:
				self.socket.connect(host)
			except Exception, e:
				self.socket = None
				raise e
		else:
			# Connect to a TCP socket
			ix = host.rfind(":")
			if(ix == -1):
				raise InvalidHostException, 'hostname "%s" is missing a port' % host
			addr = (host[:ix], int(host[ix+1:]))
			self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
			try:
				self.socket.connect(addr)
			except Exception, e:
				self.socket = None
				raise e
	
	# Fetch and parse a message from museekd, blocks until an entire message is read
	def fetch(self):
		## Unpack the first 8 bytes of the message
		data = ""
		while len(data) < 8:
			buf = self.socket.recv(8 - len(data))
			if not buf:
				self.cb_disconnected()
				self.socket = None
				return
			data += buf
	
		## First 4 bytes are the length
		length = struct.unpack("<i", data[:4])[0]
		
		if length < 4:
			raise InvalidMessageException, 'received invalid message length (%i)' % length
		## Second 4 bytes are the message code
		code = struct.unpack("<I", data[4:])[0]
		## If message is longer than it's code, unpack all data

		data = ''
		if length > 4:
			length -= 4
			while len(data) < length:
				recv = self.socket.recv(length  - len(data))
				if not recv:
					self.cb_disconnected()
					self.socket = None
					return
				
				data += recv
	
		## If message doesn't match known messages, raise an error
		if not code in MSGTAB:
			raise UnknownMessageException, 'received unknown message tyoe 0x%04X' % code
		## Parse message with the message's class parse function
		m = MSGTAB[code]()
		m.cipher = self.cipher
		return m.parse(data)
	
	# Send a message to museekd
	def send(self, message):
		message.cipher = self.cipher
		data = message.make()
		self.socket.send(message.pack_int(len(data)) + data)
	
	def close(self):
		self.socket.shutdown(1)
		
	# Fetch and process a message from museekd
	def process(self, message = None):
		if message is None:
			message = self.fetch()
			if not message:
				return
		if not self.socket:
			return
		
		if message.__class__ is messages.Challenge:
			chresp = sha256Block(message.challenge + self.password).hexdigest()
			self.send(messages.Login("SHA256", chresp, self.mask))
		elif message.__class__ is messages.Login:
			self.logged_in = message.result
			if not self.logged_in:
				self.cb_login_error(message.msg)
			else:
				self.cb_login_ok()
				self.cipher = Cipher(self.password)
		elif message.__class__ is messages.Ping:
			self.cb_ping()
		elif message.__class__ is messages.ServerState:
			self.cb_server_state(message.state, message.username)
		elif message.__class__ is messages.CheckPrivileges:
			self.cb_server_privileges(message.time_left)
		elif message.__class__ is messages.SetStatus:
			self.cb_server_status_set(message.status)
		elif message.__class__ is messages.StatusMessage:
			self.cb_status_message(message.type, message.message)
		elif message.__class__ is messages.DebugMessage:
			self.cb_debug_message(message.domain, message.message)
		elif message.__class__ is messages.ConfigState:
			self.cb_config_state(message.config)
		elif message.__class__ is messages.ConfigSet:
			self.cb_config_set(message.domain, message.key, message.value)
		elif message.__class__ is messages.ConfigRemove:
			self.cb_config_remove(message.domain, message.key)
		elif message.__class__ is messages.PeerExists:
			self.cb_peer_exists(message.user, message.exists)
		elif message.__class__ is messages.PeerStatus:
			self.cb_peer_status(message.user, message.status)
		elif message.__class__ is messages.PeerStats:
			self.cb_peer_stats(message.user, message.avgspeed, message.numdownloads, message.numfiles, message.numdirs)
		elif message.__class__ is messages.PeerAddress:
			self.cb_peer_address(message.user, message.ip, message.port)
		elif message.__class__ is messages.RoomState:
			self.cb_room_state(message.roomlist, message.joined_rooms, message.tickers)
		elif message.__class__ is messages.RoomList:
			self.cb_room_list(message.roomlist)
		elif message.__class__ is messages.GetGlobalRecommendations:
			self.cb_get_global_recommendations(message.recommendations)
		elif message.__class__ is messages.GetRecommendations:
			self.cb_get_recommendations(message.recommendations)
		elif message.__class__ is messages.GetSimilarUsers:
			self.cb_get_similar_users(message.users)
		elif message.__class__ is messages.GetItemRecommendations:
			self.cb_get_item_recommendations(message.item, message.recommendations)
		elif message.__class__ is messages.GetItemSimilarUsers:
			self.cb_get_item_similar_users( message.item, message.users)
		elif message.__class__ is messages.AddInterest:
			self.cb_add_interest(message.interest)
		elif message.__class__ is messages.RemoveInterest:
			self.cb_remove_interest(message.interest)
		elif message.__class__ is messages.AddHatedInterest:
			self.cb_add_hated_interest(message.interest)
		elif message.__class__ is messages.RemoveHatedInterest:
			self.cb_remove_hated_interest(message.interest)
		elif message.__class__ is messages.JoinRoom:
			self.cb_room_joined(message.room, message.users)
		elif message.__class__ is messages.LeaveRoom:
			self.cb_room_left(message.room)
		elif message.__class__ is messages.UserJoinedRoom:
			self.cb_room_user_joined(message.room, message.user, message.userdata)
		elif message.__class__ is messages.UserLeftRoom:
			self.cb_room_user_left(message.room, message.user)
		elif message.__class__ is messages.SayRoom:
			self.cb_room_said(message.room, message.user, message.line)
		elif message.__class__ is messages.RoomTickers:
			self.cb_room_tickers(message.room, message.tickers)
		elif message.__class__ is messages.RoomTickerSet:
			self.cb_room_ticker_set(message.room, message.user, message.message)
		elif message.__class__ is messages.PrivateMessage:
			self.cb_private_message(message.direction, message.timestamp, message.user, message.message)
		elif message.__class__ is messages.Search:
			self.cb_search_ticket(message.query, message.ticket)
		elif message.__class__ is messages.SearchReply:
			self.cb_search_results(message.ticket, message.user, message.free, message.speed, message.queue, message.results)
		elif message.__class__ is messages.UserInfo:
			self.cb_user_info(message.user, message.info, message.picture, message.uploads, message.queue, message.slotsfree)
		elif message.__class__ is messages.UserShares:
			self.cb_user_shares(message.user, message.shares)
		elif message.__class__ is messages.TransferState:
			self.cb_transfer_state(message.downloads, message.uploads)
		elif message.__class__ is messages.TransferUpdate:
			self.cb_transfer_update(message.transfer)
		elif message.__class__ is messages.TransferRemove:
			self.cb_transfer_remove(message.transfer)
		elif message.__class__ is messages.TransferAbort:
			self.cb_transfer_abort(message.transfer)
		else:
			print 'Unhandled message:', message
	
	
	# Sync with museekd
	def sync(self, ignore = False):
#		print "syncing"
		id = self.sync_id
		self.sync_id += 1
		self.send(messages.Ping(id))
		while 1:
			try:
				message = self.fetch()
			except Exception, e:
				print e
				continue
			if message.__class__ == messages.Ping and message.id == id:
				break
			elif not ignore:
				self.process(message)
#		print "synced"
	
	# disconnected
	def cb_disconnected(self):
		pass
#		print 'disconnected'
	
	# Ping
	def cb_ping(self):
		pass
#		print "pong"
	
	# Couldn't log in
	def cb_login_error(self, error):
		pass
#		print 'couldn\'t login:', error
		
	# Logged in successfully
	def cb_login_ok(self):
		pass
#		print 'logged in'
	
	# Server state
	def cb_server_state(self, state, username):
		pass
	
#		if not state:
#			print 'Not',
#		print 'connected to server.',
#		if state:
#			print 'Username: ', `username`,
#		print
	def cb_status_message(self, type, message):
		pass
	
	def cb_debug_message(self, domain, message):
		pass
	
	# Seconds of privileges left
	def cb_server_privileges(self, time_left):
		pass
#		print "%i seconds of privileges left" % time_left
	
	# Online / away
	def cb_server_status_set(self, status):
		pass
#		print "you are now %s" % (status and "away" or "online")
	
	# Config state
	def cb_config_state(self, config):
		pass
#		print config
	
	# Config changed
	def cb_config_set(self, domain, key, value):
		pass
#		print "Config changed:", domain, "--", key, "--", value
	
	# Config key removed
	def cb_config_remove(self, domain, key):
		pass
#		print "Removed key:", domain, "--", key
		
	# Does a peer exist?
	def cb_peer_exists(self, user, exists):
		pass
#		print 'User', user,
#		if exists:
#			print 'exists'
#		else:
#			print 'does not exist'
	
	# Peer status
	def cb_peer_status(self, user, status):
		pass
#		print 'User', user, 'is', ['offline', 'away', 'online'][status]
	
	# Peer stats
	def cb_peer_stats(self, user, avgspeed, numdownloads, numfiles, numdirs):
		pass
#		print 'User', user, 'avgspeed:', avgspeed,'files:', numfiles
	
	# Peer address
	def cb_peer_address(self, user, ip, port):
		pass
#		print 'User', user, 'ip:', ip, 'port:', port
	
	# Room state
	def cb_room_state(self, roomlist, joined_rooms, tickers):
		pass
#		print 'Joined', len(joined_rooms), 'room(s)'
	
	# Room list
	def cb_room_list(self, roomlist):
		pass
#		print len(roomlist), 'rooms'
	def cb_get_global_recommendations(self, recommendations):
		pass
	def cb_get_similar_users(self, users):
		pass
	def cb_get_recommendations(self, recommendations):
		pass	
	def cb_get_item_similar_users(self, item, users):
		pass
	def cb_get_item_recommendations(self, item, recommendations):
		pass	
	def cb_add_interest(self, interest):
		pass
	
	def cb_remove_interest(self, interest):
		pass
	
	def cb_add_hated_interest(self, interest):
		pass
	
	def cb_remove_hated_interest(self, interest):
		pass
	
	# Joined room
	def cb_room_joined(self, room, users):
		pass
#		print 'Joined room', room, len(users), 'users'
	
	# Left room
	def cb_room_left(self, room):
		pass
#		print 'Left room', room
	
	# User joined a room
	def cb_room_user_joined(self, room, user, userdata):
		pass
#		print 'User', user, 'joined room', room
	
	# User left a room
	def cb_room_user_left(self, room, user):
		pass
#		print 'User', user, 'left room', room
	
	# Someone said something
	def cb_room_said(self, room, user, message):
		pass
#		print '%s [%s] %s' % (room, user, message)

	# See Room tickers
	def cb_room_tickers(self, room, tickers):
		pass
#		print "Ticker set for room '%s', user '%s': %s" % (room, user, message)

	# Someone set a ticker message
	def cb_room_ticker_set(self, room, user, message):
		pass
#		print "Ticker set for room '%s', user '%s': %s" % (room, user, message)
	
	# Private message
	def cb_private_message(self, direction, timestamp, user, message):
		pass
#		print '[%s] %s' % (user, message)
	
	# Search ticket
	def cb_search_ticket(self, query, ticket):
		pass
#		print '%s: %i' % (query, ticket)
	
	# Search results
	def cb_search_results(self, ticket, user, free, speed, queue, results):
		pass
#		print len(results), 'search results from user', user
	
	# User info
	def cb_user_info(self, user, info, picture, uploads, queue, slotsfree):
		pass
#		print 'user info for', user, '\n' + info
	
	# User shares
	def cb_user_shares(self, user, shares):
		pass
#		print 'user shares for', user, len(shares), 'directories'
	
	# Transfer state
	def cb_transfer_state(self, downloads, uploads):
		pass
#		print len(downloads), 'downloads,', len(uploads), 'uploads'
	
	# Transfer update
	def cb_transfer_update(self, transfer):
		pass
#		print ['download', 'upload'][transfer.is_upload], transfer.user, transfer.path, 'updated'
	def cb_transfer_remove(self, transfer):
		pass

	def cb_transfer_abort(self, transfer):
		pass
