From 4077e16cb659407bcd5c162b10a2383f0b53138b Mon Sep 17 00:00:00 2001 From: Benny Prijono Date: Sun, 23 Aug 2009 14:26:37 +0000 Subject: Ticket #956: confbot application, initial version git-svn-id: http://svn.pjsip.org/repos/pjproject/trunk@2911 74dad513-b988-da41-8d7b-12977e46ad98 --- pjsip-apps/src/confbot/confbot.py | 445 ++++++++++++++++++++++++++++++++++++++ pjsip-apps/src/confbot/config.py | 36 +++ 2 files changed, 481 insertions(+) create mode 100644 pjsip-apps/src/confbot/confbot.py create mode 100644 pjsip-apps/src/confbot/config.py (limited to 'pjsip-apps/src/confbot') diff --git a/pjsip-apps/src/confbot/confbot.py b/pjsip-apps/src/confbot/confbot.py new file mode 100644 index 00000000..4a772aef --- /dev/null +++ b/pjsip-apps/src/confbot/confbot.py @@ -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] Where are: + list List the admins + add Add URI as admin + del Remove URI as admin + rr Reregister account to server + call Make call to the URI and add to voice conf + dc Disconnect call to URI + hold Hold call with that URI + update Send UPDATE to call with that URI + reinvite 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) + diff --git a/pjsip-apps/src/confbot/config.py b/pjsip-apps/src/confbot/config.py new file mode 100644 index 00000000..b6d284ea --- /dev/null +++ b/pjsip-apps/src/confbot/config.py @@ -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 -- cgit v1.2.3