426 lines
13 KiB
Python
426 lines
13 KiB
Python
"""
|
|
-----------------------------------------------------------------------
|
|
libyate.py
|
|
This file is a contribute of nexlab ( http://www.nexlab.it )
|
|
for the YATE Project ( http://YATE.null.ro ).
|
|
|
|
libyate.py is Copyright (C) 2005 Nexlab S.r.l
|
|
Author: Franco (nextime) Lanza <nextime@nexlab.it>
|
|
Version: 0.3
|
|
|
|
Python interface library for Yate
|
|
|
|
Yet Another Telephony Engine - a fully featured software PBX and IVR
|
|
is Copyright (C) 2004 Null Team
|
|
|
|
Thanks to:
|
|
- Dana ( my love who understand me when at 5:00 am i'm writing this piece of code )
|
|
- Diana and Paoul ( Null Team, creators of the YATE project itself )
|
|
- markit ( this guy it's so insistent to point my attenction to yate ... )
|
|
- all people of FSFE and all free software developers ( OS filosofy roks! )
|
|
- debian project ( i love this GNU\Linux distro )
|
|
|
|
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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
------------------------------------------------------------------------
|
|
|
|
Changelog:
|
|
----------
|
|
|
|
version 0.3:
|
|
- added threading support.
|
|
- modified test.py example application to do an example of how to use also the threaded version.
|
|
|
|
version 0.2:
|
|
- rewriting with asyncore support and better use of OO python power.
|
|
- added a test.py application as an example of how to use this library.
|
|
|
|
version 0.1:
|
|
- first version, it's based on a 1-to-1 port of the libyate.php library.
|
|
|
|
|
|
TODO:
|
|
-----
|
|
- here i will write few lines of documentation for the use of the lib
|
|
- add support for audio channels ( fd 4 and 5 )
|
|
|
|
"""
|
|
|
|
# various needed import
|
|
|
|
import sys
|
|
|
|
try:
|
|
import asyncore, asynchat, random, time, string
|
|
except:
|
|
sys.stdout.write("YATE PYTHON LIBRARY:\n\n")
|
|
sys.stderr.write("You need the following python modules installed:\n\n1- asyncore\n2- asynchat\n3- random\n4- time\n5- string\n6- threading\n")
|
|
sys.exit(1)
|
|
|
|
|
|
# internal class to make use of asyncore python module
|
|
class YateInit(asynchat.async_chat):
|
|
""" internal use only! """
|
|
|
|
def __init__(self, fd, handler):
|
|
asynchat.async_chat.__init__(self)
|
|
self.set_terminator("\n")
|
|
self.in_buffer = ''
|
|
self._incoming = []
|
|
self.set_file(fd)
|
|
self.handler = handler
|
|
|
|
def set_file(self, fd):
|
|
self._fileno = fd
|
|
self.socket = asyncore.file_wrapper(fd)
|
|
if fd == 0:
|
|
self.add_channel()
|
|
|
|
def handle_connect(self):
|
|
pass
|
|
|
|
def collect_incoming_data(self, data):
|
|
self.in_buffer = self.in_buffer + data
|
|
|
|
def found_terminator(self):
|
|
if self._fileno == 0:
|
|
datain = self.in_buffer
|
|
self.in_buffer = ''
|
|
if (datain != '\n') and ( datain != ''):
|
|
self.handler(datain)
|
|
|
|
def handle_close(self):
|
|
# self.write("CLOSE\n")
|
|
self.close()
|
|
|
|
def write(self, str):
|
|
self.send(str)
|
|
|
|
|
|
class Yate:
|
|
|
|
""" internal methods """
|
|
|
|
type = ''
|
|
name = ''
|
|
retval = ''
|
|
origin = ''
|
|
id = ''
|
|
handled = ''
|
|
params = []
|
|
|
|
# initialize the file descriptors to communicate with yate
|
|
# ( internal use )
|
|
def __init__(self):
|
|
self.si = YateInit(0, self)
|
|
self.so = YateInit(1, self)
|
|
self.se = YateInit(2, self)
|
|
|
|
# static function to intercept incoming message from yate
|
|
# ( internal use )
|
|
def __call__(self, data):
|
|
self.NotifyEvent(data)
|
|
|
|
# static function to parse incoming message from yate
|
|
# ( internal use )
|
|
""" Do a better and most precise parsing, funcking guy! """
|
|
def parse_incoming_data(self, data):
|
|
|
|
""" TODO: add feof check to respond with EOF when necessary """
|
|
|
|
#if data == EOF:
|
|
# sys.stderr.write("antani\n")
|
|
if data == None:
|
|
return ''
|
|
data = string.replace(data, "\n", "")
|
|
if data == '':
|
|
return ''
|
|
part = string.split(data, ":")
|
|
if part[0] == "%%>message":
|
|
# incoming message str_id:int_time:str_name:str_retval[:key=value...]
|
|
self.Yate(self.Unescape(part[3]), self.Unescape(part[4]), self.Unescape(part[1]))
|
|
self.type = "incoming"
|
|
self.origin = 0 + int(part[2])
|
|
self.params = self.FillParams(part, 5)
|
|
elif part[0] == "%%<message":
|
|
# message answer str_id:bool_handled:str_name:str_retval[:key=value...]
|
|
self.Yate(self.Unescape(part[3]), self.Unescape(part[4]), self.Unescape(part[1]))
|
|
self.type = "answer"
|
|
self.handled = self.Str2bool(part[2])
|
|
self.params = self.FillParams(part, 5)
|
|
elif part[0] == "%%<install":
|
|
# install answer num_priority:str_name:bool_success
|
|
self.Yate(self.Unescape(part[2]), "", 0+int(part[1]))
|
|
self.type = "installed"
|
|
self.handled = self.Str2bool(part[3])
|
|
elif part[0] == "%%<uninstall":
|
|
# uninstall answer num_priority:str_name:bool_success
|
|
self.Yate(self.Unescape(part[2]), "", 0+int(part[1]))
|
|
self.type = "uninstalled"
|
|
self.handled = self.Str2bool(part[3])
|
|
elif part[0] == "Error in":
|
|
# We are already in error so better stay quiet
|
|
pass
|
|
else:
|
|
self.Output("PYTHON parse error: " + data)
|
|
return self.type
|
|
|
|
# function to notify an event to the user application
|
|
# ( internal use )
|
|
def NotifyEvent(self, data):
|
|
udata = self.parse_incoming_data(data)
|
|
#self.se.write(data + "\n")
|
|
self.__Yatecall__(udata)
|
|
# function to convert params lists to escaped string form ready
|
|
# to use in a message to engine
|
|
def List2str(self, params = []):
|
|
r = ''
|
|
n = len(params)
|
|
if n > 0:
|
|
c = 0
|
|
while c < n:
|
|
r = r + ":" + self.Escape(params[c][0]) + "=" + self.Escape(params[c][1])
|
|
c = c + 1
|
|
return r
|
|
|
|
# asyncore.loop() wrapper for retrocompatibility with python prior to
|
|
# 2.4, this is needed because python 2.3 and prior asyncore.loop
|
|
# cant use the "count=1" parameter that we need to programmate exit
|
|
# from the loop.
|
|
def __loop__(self, timeout=30.0, use_poll=False, map=None, count=None):
|
|
poll_fun = asyncore.poll
|
|
if map is None:
|
|
map = asyncore.socket_map
|
|
|
|
if count is None:
|
|
while map:
|
|
poll_fun(timeout, map)
|
|
|
|
else:
|
|
while map and count > 0:
|
|
poll_fun(timeout, map)
|
|
count = count - 1
|
|
|
|
# loop executed in separate thread
|
|
def __th_loop__(self, exit_cond):
|
|
self.exit_condition = exit_cond
|
|
while not self.exit_condition:
|
|
self.flush()
|
|
|
|
# function to create a new thread with an asyncore loop
|
|
def th_loop(self):
|
|
try:
|
|
import threading
|
|
self.exit_condition = []
|
|
threading.Thread(target=self.__th_loop__, args=(self.exit_condition,)).start()
|
|
except:
|
|
self.Output("PYTHON ERROR: You need the threading python module to use threaded asyncore.loop()")
|
|
|
|
# stop a threaded loop
|
|
def th_stop(self):
|
|
self.exit_condition.append(None)
|
|
|
|
# function to be wrapped out on the user application.
|
|
# this is here only to advise the error if the function
|
|
# isn't wrapped
|
|
def __Yatecall__(self, data):
|
|
self.se.write("PYTHON ERROR: Hey dude! You MUST redeclare the __Yatecall__() in your app!\n")
|
|
|
|
""" following methods are called from the user application """
|
|
|
|
# do the asyncore loop! User MUST even call this at the end of his application
|
|
# I love pool(), so, i use it, you can use select() instead simply using asyncore.loop()
|
|
# without the "True" parameter.
|
|
def flush(self):
|
|
if sys.version >= "2.4":
|
|
asyncore.loop(0, True, count=1)
|
|
else:
|
|
self.__loop__(0, count=1)
|
|
|
|
# static function to clean close the asyncore.loop
|
|
def close(self):
|
|
asyncore.close_all()
|
|
|
|
# static function to output a string to Yate's stderr or logfile
|
|
# @param $str String to output
|
|
def Output(self, str):
|
|
self.se.write(str + '\n')
|
|
|
|
# Static replacement error handler that outputs plain text to stderr
|
|
# @param $errno Error code
|
|
# @param $errstr Error text
|
|
# @param $errfile Name of file that triggered the error
|
|
# @param $errline Line number where the error occured
|
|
""" TODO: rewrite this function to handle python exceptions? """
|
|
def ErrorHandler(self, errno, errstr, errfile, errline):
|
|
str = ' [' + errno + '] ' + errstr + ' in ' + errfile + ' line ' + errline + '\n'
|
|
if errno == 'E_USER_ERROR':
|
|
self.Output('Python FATAL: %s' % str)
|
|
sys.exit(1)
|
|
elif (errno == 'E_WARNING') or (errno == 'E_USER_WARNING'):
|
|
self.Output('Python ERROR: %s' % str)
|
|
elif (errno == 'E_NOTICE') or (errno == 'E_USER_NOTICE'):
|
|
self.Output('Python WARNING: %s' % str)
|
|
else:
|
|
self.Output('Python Unknown ERROR: %s' % str)
|
|
|
|
# Static function to convert a Yate string representation to a boolean
|
|
# @param $str String value to convert
|
|
# @return True if $str is "true", false otherwise
|
|
def Str2bool(self, str):
|
|
if str == "true":
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
# Static function to convert a boolean to a Yate string representation
|
|
# @param $bool Boolean value to convert
|
|
# @return The string "true" if $bool was true, "false" otherwise
|
|
def Bool2str(self, bool):
|
|
if bool == True:
|
|
return "true"
|
|
else:
|
|
return "false"
|
|
|
|
|
|
# Static function to convert a string to its Yate escaped format
|
|
# @param $str String to escape
|
|
# @param $extra (optional) Character to escape in addition to required ones
|
|
# @return Yate escaped string
|
|
def Escape(self, str, extra = ""):
|
|
str = str + ""
|
|
s = ""
|
|
n = len(str)
|
|
i = 0
|
|
while i < n:
|
|
c = str[i]
|
|
if( ord(c) < 32 ) or (c == extra):
|
|
c = chr(ord(c) + 64)
|
|
s = s + "%"
|
|
elif( c == "%" ):
|
|
s = s + c
|
|
s = s + c
|
|
i = i + 1
|
|
return s
|
|
|
|
|
|
# Static function to convert a Yate escaped string back to a plain string
|
|
# @param $str Yate escaped string to unescape
|
|
# @return Unescaped string
|
|
def Unescape(self, str):
|
|
s = ""
|
|
n = len(str)
|
|
i = 0
|
|
while i < n:
|
|
c = str[i]
|
|
if c == "%":
|
|
i = i + 1
|
|
c = str[i]
|
|
if c != "%":
|
|
c = chr(ord(c) - 64)
|
|
s = s + c
|
|
i = i + 1
|
|
return s
|
|
|
|
# Install a Yate message handler
|
|
# @param $name Name of the messages to handle
|
|
# @param $priority (optional) Priority to insert in chain, default 100
|
|
def Install(self, name, priority = "100"):
|
|
name = self.Escape(name)
|
|
initstr = "%%>install"
|
|
self.so.write("%s:%s:%s\n" % ( initstr, priority, name ))
|
|
#self.se.write("%s:%s:%s\n" % ( initstr, priority, name ))
|
|
self.flush()
|
|
|
|
# Uninstall a Yate message handler
|
|
# @param $name Name of the messages to stop handling
|
|
def Uninstall(self, name):
|
|
initstr = "%%>uninstall"
|
|
name = self.Escape(name)
|
|
self.so.write("%s:%s\n" % ( initstr, name))
|
|
#self.se.write("%s:%s\n" % ( initstr, name))
|
|
self.flush()
|
|
|
|
# Constructor. Creates a new outgoing message
|
|
# @param $name Name of the new message
|
|
# @param $retval (optional) Default return
|
|
# @param $id (optional) Identifier of the new message
|
|
def Yate(self, name, retval = "", id = ""):
|
|
if id == "":
|
|
rand = random.Random()
|
|
rand.seed(time.time())
|
|
if sys.version >= "2.3":
|
|
id = "PY-" + "%x" % int(int (rand.random() * time.time() * 10000000) * int(rand.random() * 1000000000000))
|
|
else:
|
|
id = "PY-" + "%x" % int(int (rand.random() * time.time())) + "%x" % int(int (rand.random() * time.time()))
|
|
self.type = "outgoing"
|
|
self.name = name
|
|
self.retval = retval
|
|
self.origin = int(time.strftime("%s"))
|
|
self.handled = 'true'
|
|
self.id = id
|
|
self.params = []
|
|
|
|
# Fill the parameter array from a text representation
|
|
# @param $parts A numerically indexed array with the key=value parameters
|
|
# @param $offs (optional) Offset in array to start processing from
|
|
def FillParams(self, parts, offs = 0):
|
|
n = len(parts)
|
|
r = ''
|
|
if n > 0:
|
|
r = []
|
|
c = 0
|
|
while c < n:
|
|
if c >= offs:
|
|
s = string.split(parts[c], "=")
|
|
r.append( [ self.Unescape(s[0]), self.Unescape(s[1]) ] )
|
|
c = c + 1
|
|
return r
|
|
|
|
# Dispatch the message to Yate for handling
|
|
# @param $message Message object to dispatch
|
|
def Dispatch(self):
|
|
if self.type != "outgoing":
|
|
self.Output("Python bug: attempt to dispatch message type: " + self.type)
|
|
return
|
|
i = self.Escape(self.id, ':')
|
|
t = str(0 + self.origin)
|
|
n = self.Escape(self.name, ':')
|
|
r = self.Escape(self.retval, ':')
|
|
p = self.List2str(self.params)
|
|
self.so.write('%%>message:' + i + ':' + t + ':' + n + ':' + r + p + '\n')
|
|
#self.se.write('%%>message:' + i + ':' + t + ':' + n + ':' + r + p + '\n')
|
|
self.type = "dispatched"
|
|
self.flush()
|
|
|
|
|
|
# Acknowledge the processing of a message passed from Yate
|
|
# @param $handled (optional) True if Yate should stop processing, default false
|
|
def Acknowledge(self):
|
|
if self.type != "incoming":
|
|
self.Output("PYTHON bug: attempt to acknowledge message type: " + self.type )
|
|
return
|
|
i = self.Escape(self.id, ':')
|
|
k = self.Bool2str(self.handled)
|
|
n = self.Escape(self.name, ':')
|
|
r = self.Escape(self.retval, ':')
|
|
p = self.List2str(self.params)
|
|
self.so.write('%%<message:' + i + ':' + k + ':' + n + ':' + r + p + '\n')
|
|
#self.se.write('%%<message:' + i + ':' + k + ':' + n + ':' + r + p + '\n')
|
|
self.type = "acknowledged"
|
|
|
|
# vi: set ts=3 sw=4 sts=4 noet
|
|
# EOF!
|