#
#	Multiuser Chat Server. (6-apr-96) by Irmen de Jong.
#
#	Implements a mud-like "chat" server to which multiple users can connect,
#	by simple telnet. Some simple commands are supported.
#	Feelings are supported, but not fully. No adverbs yet.
#
#	Usage: python chatserv.py [hostname] [portnumber]
#

import select
import string
import sys
import time
import socket

WelcomeMsg = "\r\nTELNET CHAT SERVICE...\r\n"

class connection:
	def __init__(self,sock,address,name=None):
		self.sock=sock
		self.name=name
		self.address=address

	def __del__(self):
		self.sock.close()
		self.sock=None

	def send(self,msg):
		self.sock.send(msg)
	def senderr(self,errmsg):
		self.sock.send(mkerr(errmsg))
	def sendnote(self,msg):
		self.sock.send(mknote(msg))


# The following list contain all open connections.
connections=[]

#Feeling types:
SIMP=0; DEFA=1; PERS=2; QUAD=3; PREV=4; PHYS=5; DEUX=6; SHRT=7

#feelings.. taken from LPC mud soul. Not yet fully supported.
feelings = {
"hmm":		[0,0," hmm$ \nHOW \nAT"," at"],
"burp":		[1,["rudely"],""," at"],
"wink":		[1,["suggestively"],""," at"],
"smile":	[1,["happily"],""," at"],
"point":	[1,0,""," at"],
"grin":		[1,["evilly"],""," at"],
"laugh":	[1,0,""," at"],
"nod":		[1,["solemnly"],""," at"],
"wave":		[1,["happily"],""," at"],
"cackle":	[1,["with glee"],""," at"],
"chuckle":	[1,["politely"],""," at"],
"bow":		[1,0,""," to"],
"glare":	[1,["stonily"],""," at"],
"giggle":	[1,["merrily"],""," at"],
"groan":	[1,0,""," at"],
"grunt":	[1,0,""," at"],
"growl":	[1,0,""," at"],
"snarl":	[1,0,""," at"],
"recoil":	[1,["with fear"],""," from"],
"moan":		[1,0,""," at"],
"howl":		[1,["in pain"],""," at"],
"puke":		[1,0,""," on"],
"sneeze":	[1,["loudly"],""," at"],
"spit":		[1,0,""," on"],
"stare":	[1,0,""," at"],
"whistle":	[1,["appreciatively"],""," at"],
"applaud":	[1,0,""," at"],
"agree":	[1,0,""," with"],
"disagree":	[1,0,""," with"],
"fart":		[1,0,""," at"],
"dance":	[1,0,""," with"],
"purr":		[1,0,""," at"],
"listen":	[1,0,""," to"],
"apologize":	[1,0,""," to"],
"complain":	[1,0,""," about"],
"beg":		[2,0," beg$ \nHOW"," beg$ \nWHO for mercy \nHOW"],
"shake":	[2,0," shake$ \nYOUR head \nHOW"," shake$ hands with \nWHO \nHOW"],
"grimace":	[0,0," \nHOW make$ an awful face \nAT"," at"],
"stomp":	[2,0," stomp$ \nYOUR foot \nHOW"," stomp$ on \nPOSS foot \nHOW"],
"snigger":	[1,["jeeringly"],""," at"],
"watch":	[3,["carefully"]," watch the surroundings \nHOW", " watches the surroundings \nHOW"," watch \nWHO \nHOW"," watches \nWHO \nHOW",],
"scratch":	[3,["on the head"]," scratch \nMYself \nHOW"," scratches \nMYself \nHOW"," scratch \nWHO \nHOW"," scratches \nWHO \nHOW",],
"tap":		[2,["impatiently",0,"on the shoulder"]," tap$ \nYOUR foot \nHOW"," tap$ \nWHO \nWHERE"],
"curse":	[2,0," curse$ \nWHAT \nHOW"," curse$ \nWHO \nHOW"],
"sing":		[0,0," \nHOW sing$ \nWHAT \nAT"," to"],
"mumble":	[0,0," mumble$ \nWHAT \nHOW \nAT"," to"],
"scream":	[0,["loudly"]," scream$ \nWHAT \nAT \nHOW"," at"],
"yell":		[0,["in a high pitched voice"]," yell$ \nHOW \nWHAT \nAT"," at"],
"hide":		[0,0," hide$ \nHOW behind \nWHO"],
"pounce":	[5,["playfully"],""],
"kneel":	[0,0," \nHOW fall$ on \nYOUR knees \nAT"," in front of"],
"roll":		[0,["to the ceiling"]," roll$ \nYOUR eyes \nHOW"],
"boggle":	[0,0," boggle$ \nHOW at the concept"],
"cheer":	[7,["enthusiastically"],""],
"wiggle":	[0,0," wiggle$ \nYOUR bottom \nHOW"],
"flip":		[0,0," flip$ \nHOW head over heels"],
"cry":		[6,0," cry \nHOW"," cries \nHOW"],
"sob":		[7,0,""],
"sweat":	[7,0,""],
"gurgle":	[7,0,""],
"grumble":	[7,0,""],
"die":		[6,0," fall \nHOW down and play dead"," falls \nHOW to the ground, dead"],
"stumble":	[7,0,""],
"bounce":	[7,0,""],
"yawn":		[7,0,""],
"sulk":		[7,["in the corner"],""],
"strut":	[7,["proudly"],""],
"sniff":	[7,0,""],
"snore":	[7,0,""],
"snicker":	[7,0,""],
"smirk":	[7,0,""],
"jump":		[0,["in aggravation"] ," jump$ up and down \nHOW"],
"fume":		[7,0 ,""],
"faint":	[7,0,""],
"shrug":	[7,0,""],
"pout":		[7,0,""],
"hiccup":	[7,0,""],
"frown":	[7,0,""],
"gasp":		[7,["in astonishment"],""],
"think":	[7,["carefully"],""],
"ponder":	[7,["over some problem"],""],
"clap":		[7,0,""],
"sigh":		[7,0,""],
"cough":	[7,["noisily"],""],
"shiver":	[7,["from the cold"],""],
"blush":	[6,0," blush \nHOW"," blushes \nHOW"]
}


# stripcrlf: strip CR/LF or LF from string
def stripcrlf(str):
	if len(str)>2:
		if (str[-2:]=='\r\n') or (str[-2:]=='\n\r'): return str[:-2]
	if len(str)>1:
		if str[-1]=='\n': return str[:-1]
	return str

# broadcast: send a msg to all connections, except self.
def broadcast(self_idx,msg):
	i=0
	for c in connections:
		if i!=self_idx: c.sock.send(msg)
		i=i+1

# shutdown: close connection & send msg to quitter & others
def shutdown(i,msg,brmsg):
	if brmsg: broadcast(i,brmsg)
	connections[i].send(msg)
	log(i,"QUIT")
	del connections[i]

# log: log message from connection #i (to screen)
def log(i,msg):
	if connections[i].name:
		print msg+'\t'+connections[i].name+'\t'+`connections[i].address`,time.ctime(time.time())
	else:
		print msg+"\t<???>\t"+`connections[i].address`,time.ctime(time.time())

# mkerr: convert string into error string
# mknote: convert string into note string
def mkerr(msg):
	return "** ERROR: "+msg+" **\r\n"
def mknote(msg):
	return "** NOTE: "+msg+" **\r\n"

# do_feeling: execute feeling 'data'. i=index of active socket.
def do_feeling(i,data):
	line=string.split(data)
	feeling=string.lower(line[0])[1:]
	if len(line)>1:
		args=line[1:]
	else:
		args=None

	if feeling in feelings.keys():
		adv=""
		if feelings[feeling][1]:
			adv=' '+feelings[feeling][1][0]
		connections[i].send("You "+feeling+adv+".\r\n")
		if feeling[-2:]=='ch' or feeling[-1]=='s' or feeling[-2:]=='sh':
			feeling=feeling+'e'
		if feeling[-1]=='y':
			feeling=feeling[:-1]+"ie"
		broadcast(i,connections[i].name+' '+feeling+'s'+adv+".\r\n")
	else:
		connections[i].senderr("Unknown feeling")


# do_command: execute command 'data'. i=index of active socket.
def do_command(i,data):
	line=string.split(data)
	cmd=string.lower(line[0])[1:]
	args=line[1:]

	if cmd=='name':
		if not args:
			connections[i].senderr("No name")
		elif args[0] in map(lambda x:x.name,connections):
			connections[i].senderr("Name already in use")
		else:
			broadcast(-1,mknote(connections[i].name+" now known as: "+args[0]))
			connections[i].name=args[0]
	elif cmd=="who":
		str=""
		for c in connections:
			str=str+"  "+c.name+"\r\n"
		connections[i].send(str)
	elif cmd=="quit":
		shutdown(i,"Quitting chat service.\n",mknote(connections[i].name+" LEFT"))
	elif cmd=="help":
		if args and args[0]=="feelings":
			connections[i].send("** FEELINGS **\r\n"
			"Currently, the following feeling commands have been defined:\r\n")
			s=""
			fls=feelings.keys()
			fls.sort()
			for f in fls:
				if (len(s)+len(f))>=76:
					connections[i].send(s+"\r\n")
					s=""
				s=s+f+' '
			connections[i].send(s+"\n")
		else:
			connections[i].send("** CHAT SERVICE ONLINE HELP **\r\n"
			"All normal text entered is sent to all other people, as if you 'said' it.\r\n"
			"Lines starting with ' are a special feelings line. See @help feelings.\r\n"
			"Lines starting with @ are regarded as a special command line:\r\n"
			"@help topic - help on topic\r\n"
			"@who      - who's there\r\n"
			"@name foo - set your name to 'foo'\r\n"
			"@quit     - exit chat service\r\n")
	else:
		connections[i].senderr("Bad CMD, try @help")


def serverloop(host,port):
	global feelings

	print "\nStarting telnet chat service on",host,port,"..."

	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.bind(host, port)
	s.listen(1)

	while 1:
		socklist=map(lambda x:x.sock, connections)
		ins,outs,exs = select.select(socklist+[s],[],[],5)

		for sock in ins:
			if sock==s:
				sock,addr=s.accept()
				connections.append(connection(sock,addr))
				sock.send(WelcomeMsg+"Enter your name: ")

			else:
				i = socklist.index(sock)
				data=sock.recv(512)
				if data:
					data=string.strip(stripcrlf(data))

					# If we don't have a name yet, read the name.
					if not connections[i].name:
						if len(data)<2:
							shutdown(i,mkerr("Name too short, try again"),None)
						elif data in map(lambda x:x.name,connections):
							shutdown(i,mkerr("Name already in use, try again"),None)
						else:
							connections[i].name=data
							broadcast(i,mknote(connections[i].name+" ARRIVES"))
							connections[i].sendnote("Ok, enter chatter or @help for help")
							log(i,"CONNECT")
							
					elif data:
						if data[0]=='\'':
							do_feeling(i,data)
						elif data[0]=='@':
							do_command(i,data)
						else:
							# normal chatter
							broadcast(i,connections[i].name+" says: `"+data+"'\r\n")

					if sock.fileno()>0: sock.send("> ")

				else:
					# EOF read or closed connection.
					shutdown(i,"Quitting chat service.\r\n",mknote(connections[i].name+" LEAVES"))


def main(host,port):
	global connections

	serverloop(host,port)
	print "Server shut down."
	del connections			# close all connections


if __name__=='__main__':
	argc=len(sys.argv)
	if argc==1:
		main(socket.gethostname(),9999)
	elif argc==2:
		main(socket.gethostname(),eval(sys.argv[1]))
	elif argc==3:
		main(host=sys.argv[1],port=eval(sys.argv[2]))
	else:
		print "Usage: python",sys.argv[0],"[hostname] [portnumber]"
