Index: pjproject/trunk/pjsip-apps/src/confbot/confbot.py
===================================================================
--- pjproject/trunk/pjsip-apps/src/confbot/confbot.py	(revision 2911)
+++ pjproject/trunk/pjsip-apps/src/confbot/confbot.py	(revision 2911)
@@ -0,0 +1,445 @@
+# $Id$
+#
+# 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
+	
+	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 + " incall"
+		if (self.in_chat):
+			str = str + " inchat "
+		if (self.im_error):
+			str = str + " imerror"
+		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:
+			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 transfering 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 = {}
+
+	def DEBUG(self, msg, level=TRACE):
+		print msg,
+		
+	def helpstring(self):
+		return """
+--h[elp]            Display this help screen
+--j[oin]            Join the chat room
+
+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)
+		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 = __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_cmd(self, member, from_uri, body):
+		body = body.strip(" \t\r\n")
+		msg = ""
+		handled = True
+		if body=="--list" or body=="--l":
+			msg = self.listmembers()
+			if msg=="":
+				msg = "Nobody is here"
+		elif body=="--show" or body=="--s":
+			msg = self.showsettings()
+		elif body=="--help" or body=="--h":
+			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=="--join" or body=="--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()
+		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)
+		pass
+
+	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)
+
Index: pjproject/trunk/pjsip-apps/src/confbot/config.py
===================================================================
--- pjproject/trunk/pjsip-apps/src/confbot/config.py	(revision 2911)
+++ pjproject/trunk/pjsip-apps/src/confbot/config.py	(revision 2911)
@@ -0,0 +1,36 @@
+# $Id$
+#
+# Confbot settings
+#
+import pjsua as pj
+
+# acc_cfg holds the account config (set it to None to disable account)
+acc_cfg = pj.AccountConfig()
+acc_cfg.id = "sip:102@pjsip.org"
+acc_cfg.reg_uri = "sip:pjsip.org"
+acc_cfg.proxy = [ "sip:pjsip.org;lr;transport=tcp" ]
+acc_cfg.auth_cred = [ pj.AuthCred("*", "102", "pw102") ]
+acc_cfg.publish_enabled = True
+acc_cfg.require_timer = True
+
+# Transport configs (set them to None to disable the transport)
+udp_cfg = pj.TransportConfig(5080)
+tcp_cfg = pj.TransportConfig(0)
+#tcp_cfg = None
+
+# Logging Config (you can also set it to None to use default values)
+def log_cb(level, str, len):
+	print str,
+	
+log_cfg = pj.LogConfig()
+#log_cfg.callback = log_cb
+
+# UA Config (you can also set it to None to use default values)
+ua_cfg = pj.UAConfig()
+ua_cfg.user_agent = "PJSIP ConfBot"
+ua_cfg.stun_host = "stun.pjsip.org"
+
+# Media config (you can also set it to None to use default values)
+media_cfg = pj.MediaConfig()
+media_cfg.enable_ice = True
+media_cfg.max_calls = 20
