yate/scripts/libyate.py

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!