"SSE/XMLHttpRequest server framework"
"Copyright 2007 J.Hartikainen"
"my.opera.com/zomg"

from twisted.web import resource, server, http
from twisted.internet import reactor
import time
import thread

class SSEClient:
	"represents the SSE stream user"
	lastMessageTime = 0
	sessionID = None
	clientIP = None	
	
	def __init__(self, service, request):
		(self.service, self.request) = (service, request)
		
		#save client IP address
		self.clientIP = request.getClientIP()
		
		#Set session ID from current timestamp
		self.sessionID = time.time()
		
		#set callbacks to stop when the client closes the connection
		self.deferred = request.notifyFinish()
		self.deferred.addCallback(self.stop)
		self.deferred.addErrback(self.stop)		

	def stop(self, reason):
		"client closed the connection"
		print self.clientIP + " closed connection"
		self.service.drop(self)

	def sendMsg(self, type, data):
		"send a message to this client"
		d = ''	
		if isinstance(data,list):
			for n in data:
				d += 'data: '+str(n)+'\n'
			
			self.request.write("Event: "+type+"\n"+d+"\n\n")	
			
		else:
			d = str(data)			
			self.request.write("Event: "+type+"\ndata: "+d+"\n\n")	
			
		self.lastMessageTime = time.time()
		
		
		
		
class SSEServer(resource.Resource):
	"The actual server handling get requests etc."
	isLeaf = True
	services = {}

	def __init__(self,services,pingUsers,readConsole):
		resource.Resource.__init__(self)		
		self.services = services			
		
		#clears the screen
		print '\033[2J'
		
		print "SSE Server starting..."
		
		if pingUsers:
			thread.start_new_thread(doPing,(self,None))
			print "Pinger thread started"
			
		if readConsole:
			thread.start_new_thread(consoleInput,(self,None))
			print "Console input listener started"
			print "Type /list for a list of running services"
			print "To send a message to all users of a service, type"
			print "<servicename> <message>"
			print "To send a message to all users of all services, type"
			print "<message>"					
		
		print "SSE Server up and running"

	def render_GET(self, request):
		"Handle HTTP GET requests"								
		
		path = request.path
		
		#remove the trailing / character from the request path
		if len(path) > 1:
			path = request.path.rstrip('/')
		
		
		
		#look if theres a service defined for the path
		if self.services.has_key(path):
			print "Service request to", self.services[path].name
			
			return self.services[path].parse(request)			

		else:
			return ''


def doPing(server,dummy):
	"used by the ping thread to ping clients"
	while 1:
		for service in server.services.values():
			service.ping()
			time.sleep(30)
				
def consoleInput(server,dummy):
	while 1:
		data = raw_input('> ')
			
		if data.startswith('/'):
			if data == '/list':
				print 'Service list:'
						
				for k in server.services.keys():
					print k + ": " + server.services[k].name + ", " + `len(server.services[k].sessions)` + " client(s)"
		
		else:
			found = False
			for k in server.services.values():
				if data.startswith(k.name+' '):

					slen = len(k.name)+1
					elen = data.find(' ',slen)
					cmd = data[slen:elen]
					param = data[elen+1:]

					k.parseAdminCommand(cmd,param)
					found = True
			
			if not found:
				print "Global console broadcast"
				print data
				for k in server.services.values():													
					k.broadcast('console-message',data)		
		
class Service:

	def __init__(self,name):
		self.name = name
		self.sessions = []

	def parse(self,request):
		"do something fun"
		
		
	def findClient(self,request):
		"Can be used to find the SSE session of the client, if available"
		
		for s in self.sessions:
			#The GET variable used for the session ID is 'sid'
			if request.args.has_key('sid'):
				
				#the sid variable and client IP must match a sessions variables
				if str(request.args['sid'][0]) == str(s.sessionID) and request.getClientIP() == s.clientIP:					
					return s
					
		return None
				
	def startSession(self,client):		
		#Indicates that we will close the connection
		#when we have sent all the data we have
		client.request.setHeader('Connection','Close')
		
		#SSE content type header
		client.request.setHeader('Content-Type','application/x-dom-event-stream')
		
		#No caching
		client.request.setHeader('Cache-Control','no-cache, must-revalidate')
		client.request.setHeader('Expires','Mon, 26 Jul 1997 05:00:00 GMT')
		
		#add client to session list
		self.sessions.append(client)				
		
		#tell the user his/her session ID
		client.sendMsg('session-id',client.sessionID)
		
		#do not close connection		
		return server.NOT_DONE_YET
		#return 'HTTP/1.1 200 OK\r\n'
	
	
	def broadcast(self,type,message):
		for s in self.sessions:
			s.sendMsg(type,message)

	def parseAdminCommand(self,cmd,param):
		if cmd == 'msg':
			self.broadcast('console-message',param)

		if cmd == 'dropclient':
			client = self.findClientByIP(param)
			if client == None:
				print "Can't find client with IP",param
			else:
				print "Dropping client from",param
				client.stop('Dropped by admin')

	def findClientByIP(self,ip):
		for s in self.sessions:
			if s.clientIP == ip:
				return s

		return None

	def ping(self):		
		for s in self.sessions:
			if s.lastMessageTime > 60:
				s.sendMsg('ping','pong')

	def drop(self,s):
		self.sessions.remove(s)
