#
# SIP Conference Bot
#
# Copyright (C) 2008-2009 Teluu Inc. (http://www.teluu.com)
#
# 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 pjsua as pj
import string
import sys

CFG_FILE = "config"

INFO = 1
TRACE = 2
	
# Call callback. This would just forward the event to the Member class
class CallCb(pj.CallCallback):
	def __init__(self, member, call=None):
		pj.CallCallback.__init__(self, call)
		self.member = member
	
	def on_state(self):
		self.member.on_call_state(self.call)
	
	def on_media_state(self):
		self.member.on_call_media_state(self.call)

	def on_dtmf_digit(self, digits):
		self.member.on_call_dtmf_digit(self.call, digits)

	def on_transfer_request(self, dst, code):
		return self.member.on_call_transfer_request(self.call, dst, code)
	
	def on_transfer_status(self, code, reason, final, cont):
		return self.member.on_call_transfer_status(self.call, code, reason, final, cont)

	def on_replace_request(self, code, reason):
		return self.member.on_call_replace_request(self.call, code, reason)
		
	def on_replaced(self, new_call):
		self.member.on_call_replaced(self.call, new_call)

	def on_typing(self, is_typing):
		self.member.on_typing(is_typing, call=self.call)

	def on_pager(self, mime_type, body):
		self.member.on_pager(mime_type, body, call=self.call)
	
	def on_pager_status(self, body, im_id, code, reason):
		self.member.on_pager_status(body, im_id, code, reason, call=self.call)

# Buddy callback. This would just forward the event to Member class
class BuddyCb(pj.BuddyCallback):
	def __init__(self, member, buddy=None):
		pj.BuddyCallback.__init__(self, buddy)
		self.member = member
	
	def on_pager(self, mime_type, body):
		self.member.on_pager(mime_type, body, buddy=self.buddy)

	def on_pager_status(self, body, im_id, code, reason):
		self.member.on_pager_status(body, im_id, code, reason, buddy=self.buddy)

	def on_state(self):
		self.member.on_pres_state(self.buddy)

	def on_typing(self, is_typing):
		self.member.on_typing(is_typing, buddy=self.buddy)




##############################################################################
#
#
# This class represents individual room member (either/both chat and voice conf)
#
#
class Member:
	def __init__(self, bot, uri):
		self.uri = uri
		self.bot = bot
		self.call = None
		self.buddy = None
		self.bi = pj.BuddyInfo()
		self.in_chat = False
		self.in_voice = False
		self.im_error = False
		self.html = False
	
	def __str__(self):
		str = string.ljust(self.uri, 30) + " -- " 
		if self.buddy:
			bi = self.buddy.info()
			str = str + bi.online_text
		else:
			str = str + "Offline"
		str = str + " ["
		if (self.in_voice):
			str = str + " voice"
		if (self.in_chat):
			str = str + " chat"
			if (self.html):
				str = str + " html"
			else:
				str = str + " plain"

		if (self.im_error):
			str = str + " im_error"
		str = str + "]"
		return str
		
	def join_call(self, call):
		if self.call:
			self.call.hangup(603, "You have been disconnected for making another call")
		self.call = call
		call.set_callback(CallCb(self, call))
		msg = "%(uri)s is attempting to join the voice conference" % \
			  {'uri': self.uri}
		self.bot.DEBUG(msg + "\n", INFO)
		self.bot.broadcast_pager(None, msg)
	
	def join_chat(self):
		if not self.buddy:
			self.bot.DEBUG(self.uri + " joining chatroom...\n", INFO)
			self.buddy = self.bot.acc.add_buddy(self.uri)
			self.buddy.set_callback(BuddyCb(self, self.buddy))
			self.buddy.subscribe()
		else:
			self.bot.DEBUG(self.uri + " already in chatroom, resubscribing..\n", INFO)
			self.buddy.subscribe()

	def send_pager(self, body, mime="text/plain"):
		self.bot.DEBUG("send_pager() to " + self.uri)
		if self.in_chat and not self.im_error and self.buddy:
			if self.html:
				#This will make us receive html!
				#mime = "text/html"
				body = body.replace("<", "&lt;")
				body = body.replace(">", "&gt;")
				body = body.replace('"', "&quot;")
				body = body.replace("\n", "<BR>\n")
			self.buddy.send_pager(body, content_type=mime)
			self.bot.DEBUG("..sent\n")
		else:
			self.bot.DEBUG("..not sent!\n")
		
	def on_call_state(self, call):
		ci = call.info()
		if ci.state==pj.CallState.DISCONNECTED:
			if self.in_voice:
				msg = "%(uri)s has left the voice conference (%(1)d/%(2)s)" % \
					  {'uri': self.uri, '1': ci.last_code, '2': ci.last_reason}
				self.bot.DEBUG(msg + "\n", INFO)
				self.bot.broadcast_pager(None, msg)
			self.in_voice = False
			self.call = None
			self.bot.on_member_left(self)
		elif ci.state==pj.CallState.CONFIRMED:
			msg = "%(uri)s has joined the voice conference" % \
				  {'uri': self.uri}
			self.bot.DEBUG(msg + "\n", INFO)
			self.bot.broadcast_pager(None, msg)
	
	def on_call_media_state(self, call):
		self.bot.DEBUG("Member.on_call_media_state\n")
		ci = call.info()
		if ci.conf_slot!=-1:
			if not self.in_voice:
				msg = self.uri + " call media is active"
				self.bot.broadcast_pager(None, msg)
			self.in_voice = True
			self.bot.add_to_voice_conf(self)
		else:
			if self.in_voice:
				msg = self.uri + " call media is inactive"
				self.bot.broadcast_pager(None, msg)
			self.in_voice = False

	def on_call_dtmf_digit(self, call, digits):
		msg = "%(uri)s sent DTMF digits %(dig)s" % \
			  {'uri': self.uri, 'dig': digits}
		self.bot.broadcast_pager(None, msg)

	def on_call_transfer_request(self, call, dst, code):
		msg = "%(uri)s is transferring the call to %(dst)s" % \
			  {'uri': self.uri, 'dst': dst}
		self.bot.broadcast_pager(None, msg)
		return 202
	
	def on_call_transfer_status(self, call, code, reason, final, cont):
		msg = "%(uri)s call transfer status is %(code)d/%(res)s" % \
			  {'uri': self.uri, 'code': code, 'res': reason}
		self.bot.broadcast_pager(None, msg)
		return True

	def on_call_replace_request(self, call, code, reason):
		msg = "%(uri)s is requesting call replace" % \
			  {'uri': self.uri}
		self.bot.broadcast_pager(None, msg)
		return (code, reason)
		
	def on_call_replaced(self, call, new_call):
		msg = "%(uri)s call is replaced" % \
			  {'uri': self.uri}
		self.bot.broadcast_pager(None, msg)

	def on_pres_state(self, buddy):
		old_bi = self.bi
		self.bi = buddy.info()
		msg = "%(uri)s status is %(st)s" % \
		  {'uri': self.uri, 'st': self.bi.online_text}
		self.bot.DEBUG(msg + "\n", INFO)
		self.bot.broadcast_pager(self, msg)

		if self.bi.sub_state==pj.SubscriptionState.ACTIVE:
			if not self.in_chat:
				self.in_chat = True
				buddy.send_pager("Welcome to chatroom")
				self.bot.broadcast_pager(self, self.uri + " has joined the chat room")
			else:
				self.in_chat = True
		elif self.bi.sub_state==pj.SubscriptionState.NULL or \
			 self.bi.sub_state==pj.SubscriptionState.TERMINATED or \
			 self.bi.sub_state==pj.SubscriptionState.UNKNOWN:
			self.buddy.delete()
			self.buddy = None
			if self.in_chat:
				self.in_chat = False
				self.bot.broadcast_pager(self, self.uri + " has left the chat room")
			else:
				self.in_chat = False
			self.bot.on_member_left(self)
		
	def on_typing(self, is_typing, call=None, buddy=None):
		if is_typing:
			msg = self.uri + " is typing..."
		else:
			msg = self.uri + " has stopped typing"
		self.bot.broadcast_pager(self, msg)

	def on_pager(self, mime_type, body, call=None, buddy=None):
		if not self.bot.handle_cmd(self, None, body):
			msg = self.uri + ": " + body
			self.bot.broadcast_pager(self, msg, mime_type)
	
	def on_pager_status(self, body, im_id, code, reason, call=None, buddy=None):
		self.im_error = (code/100 != 2)



##############################################################################
#
#
# The Bot instance (singleton)
#
#
class Bot(pj.AccountCallback):
	def __init__(self):
		pj.AccountCallback.__init__(self, None)
		self.lib = pj.Lib()
		self.acc = None
		self.calls = []
		self.members = {}
		self.cfg = None

	def DEBUG(self, msg, level=TRACE):
		print msg,
		
	def helpstring(self):
		return """
--h[elp]            Display this help screen
--j[oin]            Join the chat room
--html on|off       Set to receive HTML or plain text

Participant commands:
--s[how]            Show confbot settings
--leave             Leave the chatroom
--l[ist]            List all members

Admin commands:
--a[dmin] <CMD> Where <CMD> are:
    list            List the admins
    add <URI>       Add URI as admin
    del <URI>       Remove URI as admin
    rr              Reregister account to server
    call <URI>      Make call to the URI and add to voice conf
    dc <URI>        Disconnect call to URI
    hold <URI>      Hold call with that URI
    update <URI>    Send UPDATE to call with that URI
    reinvite <URI>  Send re-INVITE to call with that URI
"""

	def listmembers(self):
		msg = ""
		for uri, m in self.members.iteritems():
			msg = msg + str(m) + "\n"
		return msg
	
	def showsettings(self):
		ai = self.acc.info()
		msg = """
ConfBot status and settings:
  URI:        %(uri)s
  Status:     %(pres)s
  Reg Status: %(reg_st)d
  Reg Reason: %(reg_res)s
""" % {'uri': ai.uri, 'pres': ai.online_text, \
	   'reg_st': ai.reg_status, 'reg_res': ai.reg_reason}
		return msg
  
	def main(self, cfg_file):
		try:
			cfg = self.cfg = __import__(cfg_file)
			
			self.lib.init(ua_cfg=cfg.ua_cfg, log_cfg=cfg.log_cfg, media_cfg=cfg.media_cfg)
			self.lib.set_null_snd_dev()
			
			transport = None
			if cfg.udp_cfg:
				transport = self.lib.create_transport(pj.TransportType.UDP, cfg.udp_cfg)
			if cfg.tcp_cfg:
				t = self.lib.create_transport(pj.TransportType.TCP, cfg.tcp_cfg)
				if not transport:
					transport = t
				
			self.lib.start()
			
			if cfg.acc_cfg:
				self.DEBUG("Creating account %(uri)s..\n" % {'uri': cfg.acc_cfg.id}, INFO)
				self.acc = self.lib.create_account(cfg.acc_cfg, cb=self)
			else:
				self.DEBUG("Creating account for %(t)s..\n" % \
							{'t': transport.info().description}, INFO)
				self.acc = self.lib.create_account_for_transport(transport, cb=self)
			
			self.acc.set_basic_status(True)
			
			# Wait for ENTER before quitting
			print "Press q to quit or --help/--h for help"
			while True:
				input = sys.stdin.readline().strip(" \t\r\n")
				if not self.handle_cmd(None, None, input):
					if input=="q":
						break
			
			self.lib.destroy()
			self.lib = None

		except pj.Error, e:
			print "Exception: " + str(e)
			if self.lib:
				self.lib.destroy()
				self.lib = None
	
	def broadcast_pager(self, exclude_member, body, mime_type="text/plain"):
		self.DEBUG("Broadcast: " + body + "\n")
		for uri, m in self.members.iteritems():
			if m != exclude_member:
				m.send_pager(body, mime_type)

	def add_to_voice_conf(self, member):
		if not member.call:
			return
		src_ci = member.call.info()
		self.DEBUG("bot.add_to_voice_conf\n")
		for uri, m in self.members.iteritems():
			if m==member:
				continue
			if not m.call:
				continue
			dst_ci = m.call.info()
			if dst_ci.media_state==pj.MediaState.ACTIVE and dst_ci.conf_slot!=-1:
				self.lib.conf_connect(src_ci.conf_slot, dst_ci.conf_slot)
				self.lib.conf_connect(dst_ci.conf_slot, src_ci.conf_slot)
	
	def on_member_left(self, member):
		if not member.call and not member.buddy:
			del self.members[member.uri]
			del member
			
	def handle_admin_cmd(self, member, body):
		if member and self.cfg.admins and not member.uri in self.cfg.admins:
			member.send_pager("You are not admin")
			return
		args = body.split()
		msg = ""

		if len(args)==1:
			args.append(" ")

		if args[1]=="list":
			if not self.cfg.admins:
				msg = "Everyone is admin!"
			else:
				msg = str(self.cfg.admins)
		elif args[1]=="add":
			if len(args)!=3:
				msg = "Usage: add <URI>"
			else:
				self.cfg.admins.append(args[2])
				msg = args[2] + " added as admin"
		elif args[1]=="del":
			if len(args)!=3:
				msg = "Usage: del <URI>"
			elif args[2] not in self.cfg.admins:
				msg = args[2] + " is not admin"
			else:
				self.cfg.admins.remove(args[2])
				msg = args[2] + " has been removed from admins"
		elif args[1]=="rr":
			msg = "Reregistering.."
			self.acc.set_registration(True)
		elif args[1]=="call":
			if len(args)!=3:
				msg = "Usage: call <URI>"
			else:
				uri = args[2]
				try:
					call = self.acc.make_call(uri)
				except pj.Error, e:
					msg = "Error: " + str(e)
					call = None

				if call:
					if not uri in self.members:
						m = Member(self, uri)
						self.members[m.uri] = m
					else:
						m = self.members[uri]
					msg = "Adding " + m.uri + " to voice conference.."
					m.join_call(call)
		elif args[1]=="dc" or args[1]=="hold" or args[1]=="update" or args[1]=="reinvite":
			if len(args)!=3:
				msg = "Usage: " + args[1] + " <URI>"
			else:
				uri = args[2]
				if not uri in self.members:
					msg = "Member not found/URI doesn't match (note: case matters!)"
				else:
					m = self.members[uri]
					if m.call:
						if args[1]=="dc":
							msg = "Disconnecting.."
							m.call.hangup(603, "You're disconnected by admin")
						elif args[1]=="hold":
							msg = "Holding the call"
							m.call.hold()
						elif args[1]=="update":
							msg = "Sending UPDATE"
							m.call.update()
						elif args[1]=="reinvite":
							msg = "Sending re-INVITE"
							m.call.reinvite()
					else:
						msg = "He is not in call"
		else:
			msg = "Unknown admin command " + body

		#print "msg is '%(msg)s'" % {'msg': msg}

		if True:
			if member:
				member.send_pager(msg)
			else:
				print msg

	def handle_cmd(self, member, from_uri, body):
		body = body.strip(" \t\r\n")
		msg = ""
		handled = True
		if body=="--l" or body=="--list":
			msg = self.listmembers()
			if msg=="":
				msg = "Nobody is here"
		elif body[0:3]=="--s":
			msg = self.showsettings()
		elif body[0:6]=="--html" and member:
			if body[8:11]=="off":
				member.html = False
			else:
				member.html = True
		elif body=="--h" or body=="--help":
			msg = self.helpstring()
		elif body=="--leave":
			if not member or not member.buddy:
				msg = "You are not in chatroom"
			else:
				member.buddy.unsubscribe()
		elif body[0:3]=="--j":
			if not from_uri in self.members:
				m = Member(self, from_uri)
				self.members[m.uri] = m
				self.DEBUG("Adding " + m.uri + " to chatroom\n")
				m.join_chat()
			else:
				m = self.members[from_uri]
				self.DEBUG("Adding " + m.uri + " to chatroom\n")
				m.join_chat()
		elif body[0:3]=="--a":
			self.handle_admin_cmd(member, body)
			handled = True
		else:
			handled = False

		if msg:
			if member:
				member.send_pager(msg)
			elif from_uri:
				self.acc.send_pager(from_uri, msg);
			else:
				print msg
		return handled
	
	def on_incoming_call(self, call):
		self.DEBUG("on_incoming_call from %(uri)s\n" % {'uri': call.info().remote_uri}, INFO)
		ci = call.info()
		if not ci.remote_uri in self.members:
			m = Member(self, ci.remote_uri)
			self.members[m.uri] = m
			m.join_call(call)
		else:
			m = self.members[ci.remote_uri]
			m.join_call(call)
		call.answer(200)
	
	def on_incoming_subscribe(self, buddy, from_uri, contact_uri, pres_obj):
		self.DEBUG("on_incoming_subscribe from %(uri)s\n" % from_uri, INFO)
		return (200, 'OK')

	def on_reg_state(self):
		ai = self.acc.info()
		self.DEBUG("Registration state: %(code)d/%(reason)s\n" % \
					{'code': ai.reg_status, 'reason': ai.reg_reason}, INFO)
		if ai.reg_status/100==2 and ai.reg_expires > 0:
			self.acc.set_basic_status(True)

	def on_pager(self, from_uri, contact, mime_type, body):
		body = body.strip(" \t\r\n")
		if not self.handle_cmd(None, from_uri, body):
			self.acc.send_pager(from_uri, "You have not joined the chat room. Type '--join' to join or '--help' for the help")

	def on_pager_status(self, to_uri, body, im_id, code, reason):
		pass

	def on_typing(self, from_uri, contact, is_typing):
		pass




##############################################################################
#
#
# main()
#
#
if __name__ == "__main__":
	bot = Bot()
	bot.main(CFG_FILE)

