├── .gitignore ├── Asterisk ├── CLI.py ├── Config.py ├── Logging.py ├── Manager.py ├── Util.py └── __init__.py ├── MANIFEST.in ├── README.md ├── THANKS ├── asterisk-dump ├── asterisk-recover ├── doc ├── asterisk-events.txt ├── event-handling.txt ├── license.txt ├── py-asterisk.conf.sample ├── readme.txt └── todo.txt ├── examples └── grab-callerid.txt ├── py-asterisk └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /Asterisk/CLI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Asterisk/CLI.py: Command-line wrapper around the Asterisk Manager API. 5 | ''' 6 | 7 | # pylint: disable=W0710 8 | 9 | from __future__ import absolute_import 10 | from __future__ import print_function 11 | 12 | import inspect 13 | import os 14 | import sys 15 | 16 | from Asterisk import BaseException # pylint: disable=W0622 17 | from Asterisk import Config 18 | from Asterisk import Manager 19 | import Asterisk.Util 20 | 21 | __author__ = 'David M. Wilson ' 22 | __id__ = '$Id$' 23 | 24 | 25 | class ArgumentsError(BaseException): 26 | _prefix = 'bad arguments' 27 | 28 | 29 | def usage(argv0, out_file): 30 | ''' 31 | Print command-line program usage. 32 | ''' 33 | argv0 = os.path.basename(argv0) 34 | usage_text = ''' 35 | %(argv0)s actions 36 | Show available actions and their arguments. 37 | 38 | %(argv0)s action [ [ ..]] 39 | Execute the specified action. 40 | For named arguments "--name=" syntax may also be used. 41 | 42 | %(argv0)s command "" 43 | Execute the specified Asterisk console command. 44 | 45 | %(argv0)s usage 46 | Display this message. 47 | 48 | %(argv0)s help 49 | Display usage message for the given . 50 | 51 | ''' % locals() 52 | out_file.writelines([line[6:] + '\n' for line in usage_text.splitlines()]) 53 | 54 | 55 | def show_actions(action=None): 56 | if action is None: 57 | print() 58 | print('Supported actions and their arguments.') 59 | print('======================================') 60 | print() 61 | 62 | class AllActions(Manager.CoreActions, Manager.ZapataActions): 63 | pass 64 | 65 | methods = [(name, obj) for (name, obj) in inspect.getmembers(AllActions) 66 | if inspect.ismethod(obj) and name[0] != '_'] 67 | 68 | if action is not None: 69 | methods = [x for x in methods if x[0].lower() == action.lower()] 70 | 71 | for name, method in methods: 72 | arg_spec = inspect.getargspec(method) 73 | arg_spec[0].pop(0) 74 | print(' Action:', name) 75 | fmt = inspect.formatargspec(*arg_spec)[1:-1] 76 | if fmt: 77 | print('Arguments:', fmt) 78 | lines = [x.strip() for x in method.__doc__.strip().splitlines()] 79 | print(' ' + '\n '.join(lines)) 80 | print() 81 | 82 | 83 | def execute_action(manager, argv): 84 | method_name = argv.pop(0).lower() 85 | method_dict = dict((k.lower(), v) for (k, v) in inspect.getmembers(manager) 86 | if inspect.ismethod(v)) 87 | 88 | try: 89 | method = method_dict[method_name] 90 | except KeyError: 91 | raise ArgumentsError('%r is not a valid action.' % (method_name,)) 92 | 93 | pos_args = [] 94 | kw_args = {} 95 | process_kw = True 96 | 97 | for arg in argv: 98 | if process_kw and arg == '--': 99 | process_kw = False # stop -- processing. 100 | elif process_kw and arg[:2] == '--' and '=' in arg: 101 | key, val = arg[2:].split('=', 2) 102 | kw_args[key] = val 103 | else: 104 | pos_args.append(arg) 105 | 106 | Asterisk.Util.dump_human(method(*pos_args, **kw_args)) 107 | 108 | 109 | def command_line(argv): 110 | ''' 111 | Act as a command-line tool. 112 | ''' 113 | commands = ('actions', 'action', 'command', 'usage', 'help') 114 | 115 | if len(argv) < 2: 116 | raise ArgumentsError('please specify at least one argument.') 117 | 118 | command = argv[1] 119 | 120 | if command not in commands: 121 | raise ArgumentsError('invalid arguments.') 122 | 123 | if command == 'usage': 124 | return usage(argv[0], sys.stdout) 125 | 126 | manager = Manager.Manager(*Config.Config().get_connection()) 127 | 128 | if command == 'actions': 129 | show_actions() 130 | 131 | if command == 'help': 132 | if len(argv) < 3: 133 | raise ArgumentsError('please specify an action.') 134 | 135 | show_actions(argv[2]) 136 | 137 | elif command == 'action': 138 | if len(argv) < 3: 139 | raise ArgumentsError('please specify an action.') 140 | 141 | try: 142 | execute_action(manager, argv[2:]) 143 | except TypeError: 144 | print("Bad arguments specified. Help for %s:" % (argv[2],)) 145 | show_actions(argv[2]) 146 | 147 | elif command == 'command': 148 | execute_action('command', [argv[2]]) 149 | -------------------------------------------------------------------------------- /Asterisk/Config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Asterisk/Config.py: filesystem configuration reader. 3 | ''' 4 | 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | import os, sys 10 | 11 | #configparser has changed 12 | try: 13 | import configparser 14 | except: 15 | import ConfigParser as configparser 16 | 17 | import Asterisk 18 | 19 | # Default configuration file search path: 20 | 21 | CONFIG_FILENAME = 'py-asterisk.conf' 22 | 23 | CONFIG_PATHNAMES = [ 24 | os.environ.get('PYASTERISK_CONF', ''), 25 | os.path.join(os.environ.get('HOME', ''), '.py-asterisk.conf'), 26 | os.path.join(os.environ.get('USERPROFILE', ''), 'py-asterisk.conf'), 27 | 'py-asterisk.conf', 28 | '/etc/py-asterisk.conf', 29 | '/etc/asterisk/py-asterisk.conf', 30 | ] 31 | 32 | 33 | class ConfigurationError(Asterisk.BaseException): 34 | 'This exception is raised when there is a problem with the configuration.' 35 | _prefix = 'configuration error' 36 | 37 | 38 | # pylint: disable=W0710 39 | class Config(object): 40 | def _find_config(self, config_pathname): 41 | ''' 42 | Search the filesystem paths listed in CONFIG_PATHNAMES for a regular 43 | file. 44 | Return the name of the first one found, or , if it 45 | is not None. 46 | ''' 47 | 48 | if config_pathname is None: 49 | for pathname in CONFIG_PATHNAMES: 50 | if os.path.exists(pathname): 51 | config_pathname = pathname 52 | break 53 | 54 | if config_pathname is None: 55 | raise ConfigurationError( 56 | 'Cannot find a suitable configuration file.') 57 | 58 | return config_pathname 59 | 60 | def refresh(self): 61 | 'Read py-Asterisk configuration data from the filesystem.' 62 | 63 | try: 64 | self.conf = configparser.SafeConfigParser() 65 | self.conf.readfp(open(self.config_pathname)) 66 | 67 | except configparser.Error as e: 68 | raise ConfigurationError('%r contains invalid data at line %r' % 69 | (self.config_pathname, e.lineno)) 70 | 71 | def __init__(self, config_pathname=None): 72 | config_pathname = self._find_config(config_pathname) 73 | 74 | if config_pathname is None: 75 | raise ConfigurationError('Could not find a configuration file.') 76 | 77 | self.config_pathname = config_pathname 78 | self.refresh() 79 | 80 | def get_connection(self, connection=None): 81 | ''' 82 | Return an (address, username, secret) argument tuple, suitable for 83 | initialising a Manager instance. If is specified, use 84 | the named instead of the configuration default. 85 | ''' 86 | 87 | conf = self.conf 88 | 89 | try: 90 | if connection is None: 91 | connection = conf.get('py-asterisk', 'default connection') 92 | 93 | items = dict(conf.items('connection: ' + connection)) 94 | 95 | except configparser.Error as e: 96 | raise ConfigurationError(str(e)) 97 | 98 | try: 99 | address = (items['hostname'], int(items['port'])) 100 | 101 | except ValueError: 102 | raise ConfigurationError('The port number specified is not valid.') 103 | 104 | return (address, items['username'], items['secret']) -------------------------------------------------------------------------------- /Asterisk/Logging.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Asterisk/Logging.py: extensions to the Python 2.3 logging module. 3 | ''' 4 | 5 | from __future__ import absolute_import 6 | import logging 7 | 8 | __author__ = 'David Wilson' 9 | __Id__ = '$Id$' 10 | 11 | 12 | # Add new levels. 13 | 14 | logging.STATE = logging.INFO - 1 15 | logging.PACKET = logging.DEBUG - 1 16 | logging.IO = logging.PACKET - 1 17 | 18 | logging.addLevelName(logging.STATE, 'STATE') 19 | logging.addLevelName(logging.PACKET, 'PACKET') 20 | logging.addLevelName(logging.IO, 'IO') 21 | 22 | # Attempt to find the parent logger class using the Python 2.4 API. 23 | 24 | if hasattr(logging, 'getLoggerClass'): 25 | loggerClass = logging.getLoggerClass() 26 | else: 27 | loggerClass = logging.Logger 28 | 29 | 30 | # Provide a new logger class that supports our new levels. 31 | 32 | class AsteriskLogger(loggerClass): 33 | def state(self, msg, *args, **kwargs): 34 | "Log a message with severity 'STATE' on this logger." 35 | return self.log(logging.STATE, msg, *args, **kwargs) 36 | 37 | def packet(self, msg, *args, **kwargs): 38 | "Log a message with severity 'PACKET' on this logger." 39 | return self.log(logging.PACKET, msg, *args, **kwargs) 40 | 41 | def io(self, msg, *args, **kwargs): 42 | "Log a message with severity 'IO' on this logger." 43 | return self.log(logging.IO, msg, *args, **kwargs) 44 | 45 | 46 | # Install the new system-wide logger class. 47 | 48 | logging.setLoggerClass(AsteriskLogger) 49 | 50 | 51 | # Per-instance logging mix-in. 52 | 53 | class InstanceLogger(object): 54 | def getLoggerName(self): 55 | ''' 56 | Return the name where log messages for this instance is sent. 57 | ''' 58 | 59 | return '%s.%s' % (self.__module__, self.__class__.__name__) 60 | 61 | def getLogger(self): 62 | ''' 63 | Return the Logger instance which receives debug messages for this class 64 | instance. 65 | ''' 66 | 67 | return logging.getLogger(self.getLoggerName()) 68 | -------------------------------------------------------------------------------- /Asterisk/Manager.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0302 2 | ''' 3 | Asterisk Manager and Channel objects. 4 | ''' 5 | from __future__ import absolute_import 6 | 7 | import datetime 8 | import re 9 | import socket 10 | import time 11 | 12 | import Asterisk 13 | import Asterisk.Util 14 | import Asterisk.Logging 15 | 16 | __author__ = 'David Wilson' 17 | __id__ = '$Id$' 18 | 19 | 20 | # pylint: disable=W0710, W0622 21 | 22 | # Your ParentBaseException class should provide a __str__ method that combined 23 | # _prefix and _error as ('%s: %s' % (_prefix, _error) or similar. 24 | 25 | 26 | class BaseException(Asterisk.BaseException): 27 | 'Base class for all Asterisk Manager API exceptions.' 28 | _prefix = 'Asterisk' 29 | 30 | 31 | class AuthenticationFailure(BaseException): 32 | 'This exception is raised when authentication to the PBX instance fails.' 33 | _error = 'Authentication failed.' 34 | 35 | 36 | class CommunicationError(BaseException): 37 | 'This exception is raised when the PBX responds in an unexpected manner.' 38 | def __init__(self, packet, msg=None): 39 | e = 'Unexpected response from PBX: %r\n' % (msg,) 40 | self._error = e + ': %r' % (packet,) 41 | 42 | 43 | class GoneAwayError(BaseException): 44 | 'This exception is raised when the Manager connection becomes closed.' 45 | 46 | 47 | class InternalError(BaseException): 48 | 'This exception is raised when an error occurs within a Manager object.' 49 | _prefix = 'py-Asterisk internal error' 50 | 51 | 52 | class ActionFailed(BaseException): 53 | 'This exception is raised when a PBX action fails.' 54 | _prefix = 'py-Asterisk action failed' 55 | 56 | 57 | class PermissionDenied(BaseException): 58 | ''' 59 | This exception is raised when our connection is not permitted to perform a 60 | requested action. 61 | ''' 62 | 63 | _error = 'Permission denied' 64 | 65 | 66 | class BaseChannel(Asterisk.Logging.InstanceLogger): 67 | ''' 68 | Represents a living Asterisk channel, with shortcut methods for operating 69 | on it. The object acts as a mapping, ie. you may get and set items of it. 70 | This translates to Getvar and Setvar actions on the channel. 71 | ''' 72 | 73 | def __init__(self, manager, id): 74 | ''' 75 | Initialise a new Channel object belonging to reachable via 76 | BaseManager . 77 | ''' 78 | 79 | self.manager = manager 80 | self.id = id 81 | self.log = self.getLogger() 82 | 83 | def __eq__(self, other): 84 | 'Return truth if is equal to this object.' 85 | return (self.id == other.id) and (self.manager is other.manager) 86 | 87 | def __hash__(self): 88 | 'Return the hash value of this channel.' 89 | return hash(self.id) ^ id(self.manager) 90 | 91 | def __str__(self): 92 | return self.id 93 | 94 | def __repr__(self): 95 | return '<%s.%s referencing channel %r of %r>' %\ 96 | (self.__class__.__module__, self.__class__.__name__, 97 | self.id, self.manager) 98 | 99 | def AbsoluteTimeout(self, timeout): 100 | 'Set the absolute timeout of this channel to .' 101 | return self.manager.AbsoluteTimeout(self, timeout) 102 | 103 | def ChangeMonitor(self, pathname): 104 | 'Change the monitor filename of this channel to .' 105 | return self.manager.ChangeMonitor(self, pathname) 106 | 107 | def Getvar(self, variable, default=Asterisk.Util.Unspecified): 108 | ''' 109 | Return the value of this channel's , or if variable 110 | is not set. 111 | ''' 112 | if default is Asterisk.Util.Unspecified: 113 | return self.manager.Getvar(self, variable) 114 | return self.manager.Getvar(self, variable, default) 115 | 116 | def Hangup(self): 117 | 'Hangup this channel.' 118 | return self.manager.Hangup(self) 119 | 120 | def Monitor(self, pathname, format, mix): 121 | 'Begin monitoring of this channel into using .' 122 | return self.manager.Monitor(self, pathname, format, mix) 123 | 124 | def MixMonitorMute(self, channel, direction, state=True): 125 | 'Mute or unmute of a MixMonitor recording on a .' 126 | return self.manager.MixMonitorMute(self, channel, direction, state) 127 | 128 | def Redirect(self, context, extension='s', priority=1, channel2=None): 129 | ''' 130 | Redirect this channel to of in , 131 | optionally bridging with . 132 | ''' 133 | return self.manager.Redirect(self, context, extension, 134 | priority, channel2) 135 | 136 | def SetCDRUserField(self, data, append=False): 137 | "Append or replace this channel's CDR user field with ." 138 | return self.manager.SetCDRUserField(self, data, append) 139 | 140 | def Setvar(self, variable, value): 141 | 'Set the in this channel to .' 142 | return self.manager.Setvar(self, variable, value) 143 | 144 | def Status(self): 145 | 'Return the Status() dict for this channel (wasteful!).' 146 | return self.manager.Status()[self] 147 | 148 | def StopMonitor(self): 149 | 'Stop monitoring of this channel.' 150 | return self.manager.StopMonitor(self) 151 | 152 | def __getitem__(self, key): 153 | 'Fetch as a variable from this channel.' 154 | return self.Getvar(key) 155 | 156 | def __setitem__(self, key, value): 157 | 'Set as a variable on this channel.' 158 | return self.Setvar(key, value) 159 | 160 | 161 | class ZapChannel(BaseChannel): 162 | def ZapDNDoff(self): 163 | 'Disable DND status on this Zapata driver channel.' 164 | return self.manager.ZapDNDoff(self) 165 | 166 | def ZapDNDon(self): 167 | 'Enable DND status on this Zapata driver channel.' 168 | return self.manager.ZapDNDon(self) 169 | 170 | def ZapDialOffhook(self, number): 171 | 'Off-hook dial on this Zapata driver channel.' 172 | return self.manager.ZapDialOffhook(self, number) 173 | 174 | def ZapHangup(self): 175 | 'Hangup this Zapata driver channel.' 176 | return self.manager.ZapHangup(self) 177 | 178 | def ZapTransfer(self): 179 | 'Transfer this Zapata driver channel.' 180 | return self.manager.ZapTransfer(self) 181 | 182 | 183 | class BaseManager(Asterisk.Logging.InstanceLogger): 184 | 'Base protocol implementation for the Asterisk Manager API.' 185 | 186 | _AST_BANNER_PREFIX = 'Asterisk Call Manager' 187 | 188 | def __init__(self, address, username, secret, listen_events=True, 189 | timeout=None): 190 | ''' 191 | Provide communication methods for the PBX instance running at 192 |
. Authenticate using and . Receive event 193 | information from the Manager API if is True. 194 | ''' 195 | 196 | self.address = address 197 | self.username = username 198 | self.secret = secret 199 | self.listen_events = listen_events 200 | self.events = Asterisk.Util.EventCollection() 201 | self.timeout = timeout 202 | 203 | # Configure logging: 204 | self.log = self.getLogger() 205 | self.log.debug('Initialising.') 206 | 207 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 208 | sock.settimeout(self.timeout) 209 | sock.connect(address) 210 | 211 | self.file = sock.makefile('rwb', 0) # unbuffered 212 | self.fileno = self.file.fileno 213 | 214 | self.response_buffer = [] 215 | self._authenticate() 216 | 217 | def get_channel(self, channel_id): 218 | 'Return a channel object for the given .' 219 | 220 | if channel_id[:3].lower() == 'zap': 221 | return ZapChannel(self, channel_id) 222 | return BaseChannel(self, channel_id) 223 | 224 | def _authenticate(self): 225 | 'Read the server banner and attempt to authenticate.' 226 | 227 | banner = self.file.readline().rstrip() 228 | for enc in ('utf-8', 'latin1'): 229 | try: 230 | banner = banner.decode(enc) 231 | except: 232 | pass 233 | if not banner.startswith(self._AST_BANNER_PREFIX): 234 | raise Exception('banner incorrect; got %r, expected prefix %r' % 235 | (banner, self._AST_BANNER_PREFIX)) 236 | action = { 237 | 'Username': self.username, 238 | 'Secret': self.secret 239 | } 240 | 241 | if not self.listen_events: 242 | action['Events'] = 'off' 243 | 244 | self.log.debug('Authenticating as %r/%r.', self.username, self.secret) 245 | self._write_action('Login', action) 246 | 247 | if self._read_packet().Response == 'Error': 248 | raise AuthenticationFailure('authentication failed.') 249 | 250 | self.log.debug('Authenticated as %r.', self.username) 251 | 252 | def __repr__(self): 253 | 'Return a string representation of this object.' 254 | 255 | return '<%s.%s connected as %s to %s:%d>' %\ 256 | ((self.__module__, self.__class__.__name__, 257 | self.username) + self.address) 258 | 259 | def _write_action(self, action, data=None): 260 | ''' 261 | Write an request to the Manager API, sending header keys and 262 | values from the mapping . Return the (string) action identifier 263 | on success. Values from are omitted if they are None. 264 | ''' 265 | 266 | id = str(time.time()) # Assumes microsecond precision for reliability. 267 | lines = ['Action: ' + action, 'ActionID: ' + id] 268 | 269 | if data is not None: 270 | for item in data.items(): 271 | if item[1] is not None: 272 | if not isinstance(item[1], list): 273 | lines.append('%s: %s' % item) 274 | elif isinstance(item[1], list): 275 | for param in item[1]: 276 | lines.append('%s: %s' % (item[0], param)) 277 | 278 | self.log.packet('write_action: %r', lines) 279 | 280 | for line in lines: 281 | self.file.write(line.encode() + b'\r\n') 282 | self.log.io('_write_action: send %r', line + '\r\n') 283 | 284 | self.file.write(b'\r\n') 285 | self.log.io('_write_action: send: %r', '\r\n') 286 | return id 287 | 288 | def _read_response_follows(self): 289 | ''' 290 | Continue reading the remainder of this packet, in the format sent by 291 | the "command" action. 292 | ''' 293 | self.log.debug('In _read_response_follows().') 294 | lines = [] 295 | packet = Asterisk.Util.AttributeDict({ 296 | 'Response': 'Follows', 'Lines': lines 297 | }) 298 | line_nr = 0 299 | empty_line_ts = None 300 | while True: 301 | line = self.file.readline().rstrip() 302 | for enc in ('utf-8', 'latin1'): 303 | try: 304 | line = line.decode(enc) 305 | except: 306 | pass 307 | self.log.io('_read_response_follows: recv %r', line) 308 | line_nr += 1 309 | # In some case, ActionID is the line 2 the first starting with 310 | # 'Privilege:' 311 | if line_nr in [1, 2] and line.startswith('ActionID: '): 312 | packet.ActionID = line[10:] 313 | elif line == '--END COMMAND--': 314 | self.file.readline() 315 | self.log.debug('Completed _read_response_follows().') 316 | return packet 317 | 318 | elif not line: 319 | if self.timeout: 320 | now = datetime.datetime.now() 321 | if empty_line_ts is None: 322 | empty_line_ts = now 323 | else: 324 | if (now - empty_line_ts).seconds > self.timeout: 325 | self.log.debug("Bogus asterisk 'Command' answer.'") 326 | raise CommunicationError( 327 | packet, 'expected --END COMMAND--') 328 | self.log.debug('Empty line encountered.') 329 | 330 | else: 331 | lines.append(line) 332 | 333 | def _read_packet(self, discard_events=False): 334 | ''' 335 | Read a set of packet from the Manager API, stopping when a "\r\n\r\n" 336 | sequence is read. Return the packet as a mapping. 337 | 338 | If is True, discard all Event packets and wait for a 339 | Response packet, this is used while closing down the channel. 340 | ''' 341 | 342 | packet = Asterisk.Util.AttributeDict() 343 | self.log.debug('In _read_packet().') 344 | while True: 345 | line = self.file.readline().rstrip() 346 | for enc in ('utf-8', 'latin1'): 347 | try: 348 | line = line.decode(enc) 349 | except: 350 | pass 351 | self.log.io('_read_packet: recv %r', line) 352 | 353 | if not line: 354 | if not packet: 355 | raise GoneAwayError( 356 | 'Asterisk Manager connection has gone away.') 357 | 358 | self.log.packet('_read_packet: %r', packet) 359 | 360 | if discard_events and 'Event' in packet: 361 | self.log.debug('_read_packet() discarding: %r.', packet) 362 | packet.clear() 363 | continue 364 | 365 | self.log.debug('_read_packet() completed.') 366 | return packet 367 | 368 | val = None 369 | if line.count(':') == 1 and line[-1] == ':': # Empty field: 370 | key, val = line[:-1], '' 371 | elif line.count(',') == 1 and line[0] == ' ': # ChannelVariable 372 | key, val = line[1:].split(',', 1) 373 | else: 374 | # Some asterisk features like 'XMPP' presence 375 | # send bogus packets with empty lines in the datas 376 | # We should properly fail on those packets. 377 | try: 378 | key, val = line.split(': ', 1) 379 | except: 380 | raise InternalError('Malformed packet detected: %r' 381 | % packet) 382 | if key == 'Response' and val == 'Follows': 383 | return self._read_response_follows() 384 | 385 | packet[key] = val 386 | 387 | def _dispatch_packet(self, packet): 388 | 'Feed a single packet to an event handler.' 389 | 390 | if 'Response' in packet: 391 | self.log.debug('_dispatch_packet() placed response in buffer.') 392 | self.response_buffer.append(packet) 393 | 394 | elif 'Event' in packet: 395 | self._translate_event(packet) 396 | self.log.debug('_dispatch_packet() passing event to on_Event.') 397 | self.on_Event(packet) 398 | 399 | else: 400 | raise InternalError('Unknown packet type detected: %r' % (packet,)) 401 | 402 | def _translate_response(self, packet): 403 | ''' 404 | Raise an error if the reponse packet reports failure. Convert any 405 | channel identifiers to their equivalent objects using get_channel(). 406 | ''' 407 | 408 | for key in ('Channel', 'Channel1', 'Channel2'): 409 | if key in packet: 410 | packet[key] = self.get_channel(packet[key]) 411 | 412 | if packet.Response in ('Success', 'Follows', 'Pong'): 413 | return packet 414 | 415 | if packet.Message == 'Permission denied': 416 | raise PermissionDenied(packet.Message) 417 | 418 | raise ActionFailed(packet.Message) 419 | 420 | def _translate_event(self, event): 421 | ''' 422 | Translate any objects discovered in to Python types. 423 | ''' 424 | 425 | for key in ('Channel', 'Channel1', 'Channel2'): 426 | if key in event: 427 | event[key] = self.get_channel(event[key]) 428 | 429 | def close(self): 430 | 'Log off and close the connection to the PBX.' 431 | 432 | self.log.debug('Closing down.') 433 | 434 | self._write_action('Logoff') 435 | packet = self._read_packet(discard_events=True) 436 | if packet.Response != 'Goodbye': 437 | raise CommunicationError(packet, 'expected goodbye') 438 | self.file.close() 439 | 440 | def read(self): 441 | 'Called by the parent code when activity is detected on our fd.' 442 | 443 | self.log.io('read(): Activity detected on our fd.') 444 | packet = self._read_packet() 445 | self._dispatch_packet(packet) 446 | 447 | def read_response(self, id): 448 | 'Return the response packet found for the given action .' 449 | 450 | buffer = self.response_buffer 451 | 452 | while True: 453 | if buffer: 454 | for idx, packet in enumerate(buffer): 455 | # It is an error if no ActionID is sent. 456 | # This is intentional. 457 | if packet.ActionID == id: 458 | buffer.pop(idx) 459 | packet.pop('ActionID') 460 | return packet 461 | 462 | packet = self._read_packet() 463 | 464 | if 'Event' in packet: 465 | self._dispatch_packet(packet) 466 | 467 | elif 'ActionID' not in packet: 468 | raise CommunicationError(packet, 'no ActionID') 469 | 470 | elif packet.ActionID == id: 471 | packet.pop('ActionID') 472 | return packet 473 | 474 | else: 475 | buffer.append(packet) 476 | 477 | def on_Event(self, event): 478 | 'Triggered when an event is received from the Manager.' 479 | 480 | self.events.fire(event.Event, self, event) 481 | 482 | def responses_waiting(self): 483 | 'Return truth if there are unprocessed buffered responses.' 484 | 485 | return bool(self.response_buffer) 486 | 487 | def serve_forever(self): 488 | 'Handle one event at a time until doomsday.' 489 | 490 | while True: 491 | packet = self._read_packet() 492 | self._dispatch_packet(packet) 493 | 494 | def strip_evinfo(self, event): 495 | ''' 496 | Given an event, remove it's ActionID and Event members. 497 | ''' 498 | 499 | new = event.copy() 500 | del new['ActionID'], new['Event'] 501 | return new 502 | 503 | 504 | class CoreActions(object): # pylint: disable=R0904 505 | ''' 506 | Provide methods for Manager API actions exposed by the core Asterisk 507 | engine. 508 | ''' 509 | 510 | def AbsoluteTimeout(self, channel, timeout): 511 | 'Set the absolute timeout of to .' 512 | 513 | id = self._write_action('AbsoluteTimeout', { 514 | 'Channel': channel, 515 | 'Timeout': int(timeout) 516 | }) 517 | 518 | self._translate_response(self.read_response(id)) 519 | 520 | def ChangeMonitor(self, channel, pathname): 521 | 'Change the monitor filename of to .' 522 | 523 | id = self._write_action('ChangeMonitor', { 524 | 'Channel': channel, 525 | 'File': pathname 526 | }) 527 | 528 | self._translate_response(self.read_response(id)) 529 | 530 | def Command(self, command): 531 | 'Execute console command and return its output lines.' 532 | 533 | id = self._write_action('Command', {'Command': command}) 534 | return self._translate_response( 535 | self.read_response(id)).get('Lines') or [] 536 | 537 | def ConfbridgeListRooms(self): 538 | ''' 539 | Return a list of dictionaries containing room_name, 540 | users_count, marked_users and locked. 541 | Returns empty list if no conferences active. 542 | ''' 543 | 544 | resp = self.Command('confbridge list') 545 | rooms = list() 546 | 547 | cb_re = re.compile(r'^(?P\S+)(\s+)' 548 | r'(?P\S+)(\s+)' 549 | r'(?P\S+)(\s+)' 550 | r'(?P\S+)') 551 | for line in resp[3:]: 552 | match = cb_re.search(line) 553 | if match: 554 | rooms.append(match.groupdict()) 555 | return rooms 556 | 557 | def ConfbridgeList(self, room): 558 | ''' 559 | Return a list of dictionaries containing channel, user_profile, 560 | bridge_profile, menu, caller_id and muted. 561 | Returns empty list if was not found. 562 | ''' 563 | 564 | resp = self.Command('confbridge list %s' % room) 565 | parties = list() 566 | 567 | if 'No conference bridge named' in resp[1]: 568 | return parties 569 | else: 570 | confbridge_re = re.compile(r'^(?PSIP/\S+)\s+' 571 | r'(?P\S+)?\s+' 572 | r'(?P\S+)?\s+' 573 | r'(?P\w*)\s+' 574 | r'(?P\S+)\s+' 575 | r'(?P\S+)') 576 | for line in resp: 577 | match = confbridge_re.search(line) 578 | if match: 579 | parties.append(match.groupdict()) 580 | return parties 581 | 582 | def ConfbridgeKick(self, room, channel): 583 | ''' 584 | Kicks from conference . 585 | Returns boolean. 586 | ''' 587 | 588 | resp = self.Command('confbridge kick %s %s' % (room, channel)) 589 | return ('No participant named' not in resp[1] and 590 | 'No conference bridge named' not in resp[1]) 591 | 592 | def ConfbridgeMute(self, room, channel): 593 | ''' 594 | Mutes in conference . 595 | Returns boolean. 596 | ''' 597 | 598 | resp = self.Command('confbridge mute %s %s' % (room, channel)) 599 | return ('No channel named' not in resp[1] and 600 | 'No conference bridge named' not in resp[1]) 601 | 602 | def ConfbridgeUnmute(self, room, channel): 603 | ''' 604 | Unmutes in conference . 605 | Returns boolean. 606 | ''' 607 | 608 | resp = self.Command('confbridge unmute %s %s' % (room, channel)) 609 | return ('No channel named' not in resp[1] and 610 | 'No conference bridge named' not in resp[1]) 611 | 612 | def ConfbridgeStartRecord(self, room, rFile=None): 613 | ''' 614 | Starts recording the conference . 615 | The is optional. 616 | Returns boolean. 617 | ''' 618 | 619 | if rFile: 620 | resp = self.Command('confbridge record start %s %s' 621 | % (room, rFile)) 622 | else: 623 | resp = self.Command('confbridge record start %s' % room) 624 | return 'Conference not found.' not in resp[1] 625 | 626 | def ConfbridgeStopRecord(self, room): 627 | ''' 628 | Stop recording the conference . 629 | Returns boolean. 630 | ''' 631 | 632 | resp = self.Command('confbridge record stop %s' % room) 633 | return 'Conference not found.' not in resp[1] 634 | 635 | def ConfbridgeisRecording(self, room): 636 | ''' 637 | Verify if the conference is recording. 638 | Returns boolean. 639 | ''' 640 | 641 | resp = self.CoreShowChannels() 642 | recording_re = re.compile('^ConfBridgeRecorder/conf-%s' % room) 643 | return any(recording_re.search(str(line['Channel'])) 644 | for line in resp) 645 | 646 | def DBGet(self, family, key): 647 | 'Retrieve a key from the Asterisk database' 648 | 649 | id = self._write_action('DBGet', {'Family': family, 'Key': key}) 650 | try: 651 | response = self._translate_response(self.read_response(id)) 652 | except ActionFailed as e: 653 | return str(e) 654 | if response.get('Response') == 'Success': 655 | packet = self._read_packet() 656 | return packet.get('Val') 657 | 658 | def DBPut(self, family, key, value): 659 | 'Store a value in the Asterisk database' 660 | 661 | id = self._write_action('DBPut', 662 | {'Family': family, 'Key': key, 'Val': value}) 663 | return self._translate_response(self.read_response(id)) 664 | 665 | def Events(self, categories): 666 | 'Filter received events to only those in the list .' 667 | 668 | id = self._write_action('Events', {'EventMask': ','.join(categories)}) 669 | return self._translate_response(self.read_response(id)) 670 | 671 | def ExtensionStates(self): 672 | 'Return nested dictionary of contexts, extensions and their state' 673 | 674 | hint_re = re.compile(r'\s+(\d+)@(\S+).+State:(\S+)') 675 | state_dict = dict() 676 | for line in self.Command('core show hints'): 677 | match = hint_re.search(line) 678 | if match: 679 | extension, context, state = match.groups() 680 | if context in state_dict: 681 | state_dict[context][extension] = state 682 | else: 683 | state_dict[context] = {extension: state} 684 | return state_dict 685 | 686 | def Getvar(self, channel, variable, default=Asterisk.Util.Unspecified): 687 | ''' 688 | Return the value of 's , or if 689 | is not set. 690 | ''' 691 | self.log.debug('Getvar(%r, %r, default=%r)', 692 | channel, variable, default) 693 | 694 | id = self._write_action('Getvar', { 695 | 'Channel': channel, 696 | 'Variable': variable 697 | }) 698 | 699 | response = self._translate_response(self.read_response(id)) 700 | if variable in response: 701 | value = response[variable] 702 | else: 703 | value = response['Value'] 704 | 705 | if value == '(null)': 706 | if default is Asterisk.Util.Unspecified: 707 | raise KeyError(variable) 708 | else: 709 | self.log.debug('Getvar() returning %r', default) 710 | return default 711 | 712 | self.log.debug('Getvar() returning %r', value) 713 | return value 714 | 715 | def Hangup(self, channel): 716 | 'Hangup .' 717 | 718 | id = self._write_action('Hangup', {'Channel': channel}) 719 | return self._translate_response(self.read_response(id)) 720 | 721 | def ListCommands(self): 722 | 'Return a dict of all available => items.' 723 | 724 | id = self._write_action('ListCommands') 725 | commands = self._translate_response(self.read_response(id)) 726 | del commands['Response'] 727 | return commands 728 | 729 | def Logoff(self): 730 | 'Close the connection to the PBX.' 731 | 732 | return self.close() 733 | 734 | def MailboxCount(self, mailbox): 735 | 'Return a (, ) tuple for the given .' 736 | # TODO: this can sum multiple mailboxes too. 737 | 738 | id = self._write_action('MailboxCount', {'Mailbox': mailbox}) 739 | result = self._translate_response(self.read_response(id)) 740 | return int(result.NewMessages), int(result.OldMessages) 741 | 742 | def MailboxStatus(self, mailbox): 743 | 'Return the number of messages in .' 744 | 745 | id = self._write_action('MailboxStatus', {'Mailbox': mailbox}) 746 | return int(self._translate_response(self.read_response(id))['Waiting']) 747 | 748 | def MeetMe(self): 749 | ''' 750 | Return list of dictionaries containing confnum, parties, marked, 751 | activity and creation. 752 | Returns empty list if no conferences active. 753 | ''' 754 | 755 | resp = self.Command('meetme') 756 | meetme_list = list() 757 | if resp[1] == 'No active MeetMe conferences.': 758 | return meetme_list 759 | else: 760 | meetme_re = re.compile(r'^(?P\d+)\s+' 761 | r'(?P\d+)\s+' 762 | r'(?P\S+)\s+' 763 | r'(?P\S+)\s+' 764 | r'(?P\S+)') 765 | for line in resp: 766 | match = meetme_re.search(line) 767 | if match: 768 | meetme_list.append(match.groupdict()) 769 | return meetme_list 770 | 771 | def MeetMeList(self, confnum): 772 | 'Lists users in conferences' 773 | 774 | resp = self.Command('meetme list %s' % confnum) 775 | caller_list = list() 776 | if (resp[1] == 'No active conferences.') \ 777 | or ('No such conference' in resp[1]): 778 | return caller_list 779 | else: 780 | meetme_re = re.compile(r'^User #: (?P\d+)\s+' 781 | r'(?P.+)\s+' 782 | r'Channel: (?P\S+)\s+' 783 | r'\((?P.+)\)\s+' 784 | r'(?P\S+)') 785 | for line in resp: 786 | match = meetme_re.search(line) 787 | if match: 788 | caller_list.append(match.groupdict()) 789 | return caller_list 790 | 791 | def Monitor(self, channel, pathname, format, mix): 792 | 'Begin monitoring of into using .' 793 | 794 | id = self._write_action('Monitor', { 795 | 'Channel': channel, 796 | 'File': pathname, 797 | 'Format': format, 798 | 'Mix': mix and 'yes' or 'no' 799 | }) 800 | 801 | return self._translate_response(self.read_response(id)) 802 | 803 | def MixMonitorMute(self, channel, direction, state=True): 804 | 'Mute or unmute of a MixMonitor recording on a .' 805 | 806 | id = self._write_action('MixMonitorMute', { 807 | 'Channel': channel, 808 | 'Direction': direction 809 | if direction in ['read', 'write'] else 'both', 810 | 'State': state and '1' or '0' 811 | }) 812 | 813 | return self._translate_response(self.read_response(id)) 814 | 815 | def Originate(self, channel, context=None, extension=None, priority=None, 816 | application=None, data=None, timeout=None, caller_id=None, 817 | variable=None, account=None, async_param=None, 818 | early_media=None, codecs=None, channel_id=None, 819 | other_channel_id=None, **kwargs): 820 | ''' 821 | Originate(channel, context = .., extension = .., priority = ..[, ...]) 822 | Originate(channel, application = ..[, data = ..[, ...]]) 823 | 824 | Originate a call on , bridging it to the specified dialplan 825 | extension (format 1) or application (format 2). 826 | 827 | Dialplan context to bridge with. 828 | Context extension to bridge with. 829 | Context priority to bridge with. 830 | Application to bridge with. 831 | Application parameters. 832 | Answer timeout for in milliseconds. 833 | Outgoing channel Caller ID. 834 | channel variable to set (K=V[|K2=V2[|..]]). 835 | CDR account code. 836 | Return successfully immediately. 837 | Force call bridge on early media. 838 | Comma-separated list of codecs to use for this 839 | call. 840 | Channel UniqueId to be set on the channel 841 | Channel UniqueId to be set on the second local 842 | channel. 843 | ''' 844 | 845 | # Since channel is a required parameter, no need including it here. 846 | # As a matter of fact, including it here, generates an AttributeError 847 | # because 'None' does not have an 'id' attribute which is required in 848 | # checking equality with an object like channel 849 | has_dialplan = None not in (context, extension) 850 | has_application = application is not None 851 | 852 | if has_dialplan and has_application: 853 | raise ActionFailed('Originate: dialplan and application calling ' 854 | 'style are mutually exclusive.') 855 | 856 | if not (has_dialplan or has_application): 857 | raise ActionFailed('Originate: neither dialplan or application ' 858 | 'calling style used. Refer to documentation.') 859 | 860 | if not channel: 861 | raise ActionFailed('Originate: you must specify a channel.') 862 | 863 | # compatibility of renamed params 864 | # 'async' -> 'async_param' because of 'async' and 'await' are reserved 865 | # keywords since Python 3.7 866 | if async_param is None and 'async' in kwargs: 867 | async_param = kwargs['async'] 868 | 869 | data = { 870 | 'Channel': channel, 'Context': context, 871 | 'Exten': extension, 'Priority': priority, 872 | 'Application': application, 'Data': data, 873 | 'Timeout': timeout, 'CallerID': caller_id, 874 | 'Variable': variable, 'Account': account, 875 | 'Async': int(bool(async_param)), 876 | 'EarlyMedia': int(bool(early_media)), 877 | 'Codecs': codecs, 'ChannelId': channel_id, 878 | 'OtherChannelId': other_channel_id 879 | } 880 | 881 | id = self._write_action('Originate', data) 882 | return self._translate_response(self.read_response(id)) 883 | 884 | def Originate2(self, channel, parameters): 885 | ''' 886 | Originate a call, using parameters in the mapping . 887 | Provided for compatibility with RPC bridges that do not support keyword 888 | arguments. 889 | ''' 890 | 891 | return self.Originate(channel, **parameters) 892 | 893 | def ParkedCalls(self): 894 | 'Return a nested dict describing currently parked calls.' 895 | 896 | id = self._write_action('ParkedCalls') 897 | self._translate_response(self.read_response(id)) 898 | parked = {} 899 | 900 | def ParkedCall(self, event): 901 | event = self.strip_evinfo(event) 902 | parked[event.pop('Exten')] = event 903 | 904 | def ParkedCallsComplete(self, event): # pylint: disable=W0613 905 | stop_flag[0] = True 906 | 907 | events = Asterisk.Util.EventCollection([ParkedCall, 908 | ParkedCallsComplete]) 909 | self.events += events 910 | 911 | try: 912 | stop_flag = [False] 913 | 914 | while stop_flag[0] is False: 915 | packet = self._read_packet() 916 | self._dispatch_packet(packet) 917 | 918 | finally: 919 | self.events -= events 920 | 921 | return parked 922 | 923 | def Ping(self): 924 | 'No-op to ensure the PBX is still there and keep the connection alive.' 925 | 926 | id = self._write_action('Ping') 927 | return self._translate_response(self.read_response(id)) 928 | 929 | def Bridge(self, channel1, channel2, tone): 930 | 'Bridge together two channels' 931 | 932 | id = self._write_action('Bridge', { 933 | 'Channel1': channel1, 934 | 'Channel2': channel2, 935 | 'Tone': tone 936 | }) 937 | 938 | return self._translate_response(self.read_response(id)) 939 | 940 | def PlayDTMF(self, channel, digit): 941 | 'Plays a dtmf digit on the specified channel' 942 | id = self._write_action('PlayDTMF', { 943 | 'Channel': channel, 944 | 'Digit': digit 945 | }) 946 | 947 | return self._translate_response(self.read_response(id)) 948 | 949 | def QueueAdd(self, queue, interface, penalty=0, member_name=None): 950 | 'Add to with optional and .' 951 | params = { 952 | 'Queue': queue, 953 | 'Interface': interface, 954 | 'Penalty': str(int(penalty))} 955 | if member_name: 956 | params['MemberName'] = member_name 957 | id = self._write_action('QueueAdd', params) 958 | return self._translate_response(self.read_response(id)) 959 | 960 | def QueuePause(self, queue, interface, paused): 961 | 'Pause in .' 962 | 963 | id = self._write_action('QueuePause', { 964 | 'Queue': queue, 965 | 'Interface': interface, 966 | 'Paused': paused and 'true' or 'false' 967 | }) 968 | 969 | return self._translate_response(self.read_response(id)) 970 | 971 | def QueueRemove(self, queue, interface): 972 | 'Remove from .' 973 | 974 | id = self._write_action('QueueRemove', { 975 | 'Queue': queue, 976 | 'Interface': interface 977 | }) 978 | 979 | return self._translate_response(self.read_response(id)) 980 | 981 | def QueueStatus(self): 982 | 'Return a complex nested dict describing queue statii.' 983 | 984 | id = self._write_action('QueueStatus') 985 | self._translate_response(self.read_response(id)) 986 | queues = {} 987 | 988 | def QueueParams(self, event): 989 | queue = self.strip_evinfo(event) 990 | queue['members'] = {} 991 | queue['entries'] = {} 992 | queues[queue.pop('Queue')] = queue 993 | 994 | def QueueMember(self, event): 995 | member = self.strip_evinfo(event) 996 | queues[member.pop('Queue')]['members'][ 997 | member.pop('Location')] = member 998 | 999 | def QueueEntry(self, event): 1000 | entry = self.strip_evinfo(event) 1001 | queues[entry.pop('Queue')]['entries'][event.pop('Channel')] = entry 1002 | 1003 | def QueueStatusComplete(self, event): # pylint: disable=W0613 1004 | stop_flag[0] = True 1005 | 1006 | events = Asterisk.Util.EventCollection([ 1007 | QueueParams, QueueMember, QueueEntry, QueueStatusComplete]) 1008 | self.events += events 1009 | 1010 | try: 1011 | stop_flag = [False] 1012 | 1013 | while stop_flag[0] is False: 1014 | packet = self._read_packet() 1015 | self._dispatch_packet(packet) 1016 | 1017 | finally: 1018 | self.events -= events 1019 | 1020 | return queues 1021 | 1022 | Queues = QueueStatus 1023 | 1024 | def Redirect(self, channel, context, 1025 | extension='s', priority=1, channel2=None): 1026 | ''' 1027 | Redirect to of in , 1028 | optionally bridging with 1029 | ''' 1030 | 1031 | id = self._write_action('Redirect', { 1032 | 'Channel': channel, 1033 | 'Context': context, 1034 | 'Exten': extension, 1035 | 'Priority': priority, 1036 | 'ExtraChannel': channel2 1037 | }) 1038 | 1039 | return self._translate_response(self.read_response(id)) 1040 | 1041 | def SetCDRUserField(self, channel, data, append=False): 1042 | "Append or replace 's CDR user field with '." 1043 | 1044 | id = self._write_action('SetCDRUserField', { 1045 | 'Channel': channel, 1046 | 'UserField': data, 1047 | 'Append': append and 'yes' or 'no' 1048 | }) 1049 | 1050 | return self._translate_response(self.read_response(id)) 1051 | 1052 | def Setvar(self, channel, variable, value): 1053 | 'Set of to .' 1054 | 1055 | id = self._write_action('Setvar', { 1056 | 'Channel': channel, 1057 | 'Variable': variable, 1058 | 'Value': value 1059 | }) 1060 | 1061 | return self._translate_response(self.read_response(id)) 1062 | 1063 | def SipShowPeer(self, peer): 1064 | 'Fetch the status of SIP peer .' 1065 | id = self._write_action('SIPshowpeer', {'Peer': peer}) 1066 | showpeer = self._translate_response(self.read_response(id)) 1067 | del showpeer['Response'] 1068 | return showpeer 1069 | 1070 | def SipShowRegistry(self): 1071 | 'Return a nested dict of SIP registry.' 1072 | 1073 | id = self._write_action('SIPshowregistry') 1074 | self._translate_response(self.read_response(id)) 1075 | registry = {} 1076 | 1077 | def RegistryEntry(self, event): 1078 | event = self.strip_evinfo(event) 1079 | name = event.pop('Host') 1080 | registry[name] = event 1081 | 1082 | def RegistrationsComplete(self, event): # pylint: disable=W0613 1083 | stop_flag[0] = True 1084 | 1085 | events = Asterisk.Util.EventCollection( 1086 | [RegistryEntry, RegistrationsComplete]) 1087 | self.events += events 1088 | 1089 | try: 1090 | stop_flag = [False] 1091 | 1092 | while stop_flag[0] is False: 1093 | packet = self._read_packet() 1094 | self._dispatch_packet(packet) 1095 | 1096 | finally: 1097 | self.events -= events 1098 | return registry 1099 | 1100 | def SipPeers(self): 1101 | 'Return a nested dict of SIP peers.' 1102 | 1103 | id = self._write_action('SIPpeers') 1104 | self._translate_response(self.read_response(id)) 1105 | peer = {} 1106 | 1107 | def PeerEntry(self, event): 1108 | event = self.strip_evinfo(event) 1109 | name = event.pop('ObjectName') 1110 | peer[name] = event 1111 | 1112 | def PeerlistComplete(self, event): 1113 | stop_flag[0] = True 1114 | 1115 | events = Asterisk.Util.EventCollection([PeerEntry, PeerlistComplete]) 1116 | self.events += events 1117 | 1118 | try: 1119 | stop_flag = [False] 1120 | 1121 | while stop_flag[0] == False: 1122 | packet = self._read_packet() 1123 | self._dispatch_packet(packet) 1124 | 1125 | finally: 1126 | self.events -= events 1127 | return peer 1128 | 1129 | def Status(self): 1130 | 'Return a nested dict of channel statii.' 1131 | 1132 | id = self._write_action('Status') 1133 | self._translate_response(self.read_response(id)) 1134 | channels = {} 1135 | 1136 | def Status(self, event): 1137 | event = self.strip_evinfo(event) 1138 | name = event.pop('Channel') 1139 | channels[name] = event 1140 | 1141 | def StatusComplete(self, event): # pylint: disable=W0613 1142 | stop_flag[0] = True 1143 | 1144 | events = Asterisk.Util.EventCollection([Status, StatusComplete]) 1145 | self.events += events 1146 | 1147 | try: 1148 | stop_flag = [False] 1149 | 1150 | while stop_flag[0] is False: 1151 | packet = self._read_packet() 1152 | self._dispatch_packet(packet) 1153 | 1154 | finally: 1155 | self.events -= events 1156 | return channels 1157 | 1158 | def StopMonitor(self, channel): 1159 | 'Stop monitoring of .' 1160 | 1161 | id = self._write_action('StopMonitor', {'Channel': channel}) 1162 | return self._translate_response(self.read_response(id)) 1163 | 1164 | def CoreShowChannels(self): 1165 | 'Return a list of current channels.' 1166 | 1167 | id = self._write_action('CoreShowChannels') 1168 | self._translate_response(self.read_response(id)) 1169 | 1170 | channels = [] 1171 | 1172 | def CoreShowChannel(self, event): # pylint: disable=W0613 1173 | channels.append(event) 1174 | 1175 | def CoreShowChannelsComplete(self, event): # pylint: disable=W0613 1176 | stop_flag[0] = True 1177 | 1178 | events = Asterisk.Util.EventCollection([ 1179 | CoreShowChannel, CoreShowChannelsComplete]) 1180 | self.events += events 1181 | 1182 | try: 1183 | stop_flag = [False] 1184 | 1185 | while stop_flag[0] is False: 1186 | packet = self._read_packet() 1187 | self._dispatch_packet(packet) 1188 | 1189 | finally: 1190 | self.events -= events 1191 | 1192 | return channels 1193 | 1194 | 1195 | class ZapataActions(object): 1196 | 'Provide methods for Manager API actions exposed by the Zapata driver.' 1197 | 1198 | def ZapDialOffhook(self, channel, number): 1199 | 'Off-hook dial on Zapata driver .' 1200 | 1201 | id = self._write_action('ZapDialOffhook', { 1202 | 'ZapChannel': channel, 1203 | 'Number': number 1204 | }) 1205 | 1206 | return self._translate_response(self.read_response(id)) 1207 | 1208 | def ZapDNDoff(self, channel): 1209 | 'Disable DND status on Zapata driver .' 1210 | 1211 | id = self._write_action('ZapDNDoff', {'ZapChannel': str(int(channel))}) 1212 | return self._translate_response(self.read_response(id)) 1213 | 1214 | def ZapDNDon(self, channel): 1215 | 'Enable DND status on Zapata driver .' 1216 | 1217 | id = self._write_action('ZapDNDon', {'ZapChannel': str(int(channel))}) 1218 | return self._translate_response(self.read_response(id)) 1219 | 1220 | def ZapHangup(self, channel): 1221 | 'Hangup Zapata driver .' 1222 | 1223 | id = self._write_action('ZapHangup', {'ZapChannel': str(int(channel))}) 1224 | return self._translate_response(self.read_response(id)) 1225 | 1226 | def ZapShowChannels(self): 1227 | 'Return a nested dict of Zapata driver channel statii.' 1228 | 1229 | id = self._write_action('ZapShowChannels') 1230 | self._translate_response(self.read_response(id)) 1231 | channels = {} 1232 | 1233 | def ZapShowChannels(self, event): 1234 | event = self.strip_evinfo(event) 1235 | number = int(event.pop('Channel')) 1236 | channels[number] = event 1237 | 1238 | def ZapShowChannelsComplete(self, event): # pylint: disable=W0613 1239 | stop_flag[0] = True 1240 | 1241 | events = Asterisk.Util.EventCollection([ 1242 | ZapShowChannels, ZapShowChannelsComplete]) 1243 | self.events += events 1244 | 1245 | try: 1246 | stop_flag = [False] 1247 | 1248 | while stop_flag[0] is False: 1249 | packet = self._read_packet() 1250 | self._dispatch_packet(packet) 1251 | 1252 | finally: 1253 | self.events -= events 1254 | 1255 | return channels 1256 | 1257 | def ZapTransfer(self, channel): 1258 | 'Transfer Zapata driver .' 1259 | # TODO: Does nothing on X100P. What is this for? 1260 | 1261 | id = self._write_action('ZapTransfer', {'ZapChannel': channel}) 1262 | return self._translate_response(self.read_response(id)) 1263 | 1264 | 1265 | class CoreManager(BaseManager, CoreActions, ZapataActions): 1266 | ''' 1267 | Asterisk Manager API protocol implementation and core actions, but without 1268 | event handlers. 1269 | ''' 1270 | 1271 | pass 1272 | 1273 | 1274 | class Manager(BaseManager, CoreActions, ZapataActions): 1275 | ''' 1276 | Asterisk Manager API protocol implementation, core event handler 1277 | placeholders, and core actions. 1278 | ''' 1279 | -------------------------------------------------------------------------------- /Asterisk/Util.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Asterisk/Util.py: utility classes. 3 | ''' 4 | from __future__ import absolute_import 5 | 6 | 7 | import sys 8 | 9 | import Asterisk 10 | from Asterisk import Logging 11 | 12 | __author__ = 'David Wilson' 13 | __Id__ = '$Id$' 14 | 15 | 16 | class SubscriptionError(Asterisk.BaseException): 17 | ''' 18 | This exception is raised when an attempt to register the same (event, 19 | handler) tuple twice is detected. 20 | ''' 21 | 22 | # This special unique object is used to indicate that an argument has not been 23 | # specified. It is used where None may be a valid argument value. 24 | 25 | 26 | class Unspecified(object): 27 | 'A class to represent an unspecified value that cannot be None.' 28 | 29 | def __repr__(self): 30 | return '' 31 | 32 | Unspecified = Unspecified() 33 | 34 | 35 | class AttributeDict(dict): 36 | # Fields that can have more that one ocurrency in the manager response 37 | # or event 38 | MULTI_VALUE_FIELD = ('ChanVariable', 'DestChanVariable') 39 | 40 | def __setitem__(self, key, value): 41 | # Assign the multivalue fields correctly in the dictionary 42 | if key in self.MULTI_VALUE_FIELD and '=' in value: 43 | k, v = value.split('=')[:2] 44 | if key in self: 45 | self[key][k] = v 46 | else: 47 | super(AttributeDict, self).__setitem__(key, {k: v}) 48 | else: 49 | super(AttributeDict, self).__setitem__(key, value) 50 | 51 | def __getattr__(self, key): 52 | return self[key] 53 | 54 | def __setattr__(self, key, value): 55 | self[key] = value 56 | 57 | def copy(self): 58 | return AttributeDict(iter(self.items())) 59 | 60 | 61 | class EventCollection(Logging.InstanceLogger): 62 | ''' 63 | Utility class to allow grouping and automatic registration of event. 64 | ''' 65 | 66 | def __init__(self, initial=None): 67 | ''' 68 | If is not None, register functions from the list 69 | waiting for events with the same name as the function. 70 | ''' 71 | 72 | self.subscriptions = {} 73 | self.log = self.getLogger() 74 | 75 | if initial is not None: 76 | for func in initial: 77 | self.subscribe(func.__name__, func) 78 | 79 | def subscribe(self, name, handler): 80 | ''' 81 | Subscribe callable to event named . 82 | ''' 83 | 84 | if name not in self.subscriptions: 85 | subscriptions = self.subscriptions[name] = [] 86 | else: 87 | subscriptions = self.subscriptions[name] 88 | 89 | if handler in subscriptions: 90 | raise SubscriptionError("Duplicated subscription at event %r", (name)) # pylint: disable=W0710 91 | 92 | subscriptions.append(handler) 93 | 94 | def unsubscribe(self, name, handler): 95 | 'Unsubscribe callable to event named .' 96 | self.subscriptions[name].remove(handler) 97 | 98 | def clear(self): 99 | 'Destroy all present subscriptions.' 100 | self.subscriptions.clear() 101 | 102 | def fire(self, name, *args, **kwargs): 103 | ''' 104 | Fire event passing * and ** to subscribers, 105 | returning the return value of the last called subscriber. 106 | ''' 107 | 108 | if name not in self.subscriptions: 109 | return 110 | 111 | return_value = None 112 | 113 | for subscription in self.subscriptions[name]: 114 | self.log.debug('calling %r(*%r, **%r)', subscription, args, kwargs) 115 | return_value = subscription(*args, **kwargs) 116 | 117 | return return_value 118 | 119 | def copy(self): 120 | new = self.__class__() 121 | 122 | for name, subscriptions in self.subscriptions.items(): 123 | new.subscriptions[name] = [] 124 | for subscription in subscriptions: 125 | new.subscriptions[name].append(subscription) 126 | 127 | def __iadd__(self, collection): 128 | 'Add all the events in to our collection.' 129 | 130 | if not isinstance(collection, EventCollection): 131 | raise TypeError 132 | 133 | new = self.copy() 134 | 135 | try: 136 | for name, handlers in collection.subscriptions.items(): 137 | for handler in handlers: 138 | self.subscribe(name, handler) 139 | except Exception: 140 | self.subscriptions = new.subscriptions 141 | raise 142 | 143 | return self 144 | 145 | def __isub__(self, collection): 146 | 'Remove all the events in from our collection.' 147 | 148 | if not isinstance(collection, EventCollection): 149 | raise TypeError 150 | 151 | new = self.copy() 152 | 153 | try: 154 | for name, handlers in collection.subscriptions.items(): 155 | for handler in handlers: 156 | self.unsubscribe(name, handler) 157 | except Exception: 158 | self.subscriptions = new.subscriptions 159 | raise 160 | 161 | return self 162 | 163 | 164 | def dump_packet(packet, file=sys.stdout): 165 | ''' 166 | Dump a packet in human readable form to file-like object . 167 | ''' 168 | 169 | packet = dict(packet) 170 | 171 | if 'Event' in packet: 172 | file.write('-- %s\n' % packet.pop('Event')) 173 | else: 174 | file.write('-- Response: %s\n' % packet.pop('Response')) 175 | 176 | packet = list(packet.items()) 177 | packet.sort() 178 | 179 | for tuple in packet: 180 | file.write(' %s: %s\n' % tuple) 181 | 182 | file.write('\n') 183 | 184 | 185 | def dump_human(data, file=sys.stdout, _indent=0): 186 | 187 | recursive = (dict, list, tuple, AttributeDict) 188 | 189 | def indent(a=0, i=_indent): 190 | return ' ' * (a + i) 191 | 192 | Type = type(data) 193 | 194 | if Type in (dict, AttributeDict): 195 | items = list(data.items()) 196 | items.sort() 197 | 198 | for key, val in items: 199 | file.write(indent() + str(key) + ': ') 200 | if any(isinstance(val, type_) for type_ in recursive): 201 | file.write('\n') 202 | dump_human(val, file, _indent + 1) 203 | else: 204 | dump_human(val, file, 0) 205 | 206 | elif Type in (list, tuple): 207 | for val in data: 208 | dump_human(val, file, _indent + 1) 209 | 210 | elif Type in (int, float): 211 | file.write(indent() + '%r\n' % data) 212 | 213 | elif Type is str: 214 | file.write(indent() + data + '\n') 215 | -------------------------------------------------------------------------------- /Asterisk/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Asterisk Manager API Python package. 3 | ''' 4 | 5 | __author__ = 'David Wilson' 6 | __id__ = '$Id$' 7 | 8 | try: 9 | __revision__ = int('$Rev$'.split()[1]) 10 | except: 11 | __revision__ = None 12 | 13 | __version__ = '0.1' 14 | __all__ = ['CLI', 'Config', 'Logging', 'Manager', 'Util'] 15 | 16 | 17 | cause_codes = { 18 | 0: (0, 'UNKNOWN', 'Unkown'), 19 | 1: (1, 'UNALLOCATED', 'Unallocated number'), 20 | 16: (16, 'CLEAR', 'Normal call clearing'), 21 | 17: (17, 'BUSY', 'User busy'), 22 | 18: (18, 'NOUSER', 'No user responding'), 23 | 21: (21, 'REJECTED', 'Call rejected'), 24 | 22: (22, 'CHANGED', 'Number changed'), 25 | 27: (27, 'DESTFAIL', 'Destination out of order'), 26 | 28: (28, 'NETFAIL', 'Network out of order'), 27 | 41: (41, 'TEMPFAIL', 'Temporary failure') 28 | } 29 | 30 | 31 | class BaseException(Exception): 32 | ''' 33 | Base class for all py-Asterisk exceptions. 34 | ''' 35 | 36 | _prefix = '(Base Exception)' 37 | _error = '(no error)' 38 | 39 | def __init__(self, error): 40 | self._error = error 41 | 42 | def __str__(self): 43 | return '%s: %s' % (self._prefix, self._error) 44 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.html asterisk-dump asterisk-recover py-asterisk 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2015-01-04: New maintainer found (jeansch), thanks to David Wilson for all the past work ! 2 | 3 | # 2014-04-15: I have not used Asterisk in a decade. This package urgently needs a new maintainer! 4 | 5 | # 2012-11-29: Downloads have been moved to Cheese Shop, to facilitate easier development 6 | 7 | # http://pypi.python.org/pypi/py-Asterisk 8 | 9 | -------- 10 | 11 | 12 | ## Introduction 13 | 14 | The Python Asterisk package (codenamed `py-Asterisk`) is an attempt 15 | to produce high quality, well documented Python bindings for the Asterisk 16 | Manager API. 17 | 18 | The eventual goal of the package is to allow rich specification of the Asterisk 19 | configuration in Python rather than in the quirky, unstructured, undocumented 20 | mess that we call the Asterisk configuration files. 21 | 22 | 23 | ## Working Functionality 24 | 25 | * Python package implementing a manager client and event dispatcher. 26 | * User-oriented command line interface to manager API. 27 | 28 | 29 | ## Work In Progress 30 | 31 | * Introductory documentation for developers. 32 | * Asterisk module allowing dialplan configuration via the manager API (see [PbxConfigMgr my pbx_config_mgr page]). 33 | * Objects to represent the standard applications, for specification of dialplan configuration in a friendlier Python syntax. 34 | 35 | 36 | ## Documentation 37 | 38 | Very little hand-written documentation is available yet for the package or it's 39 | commands, however pydoc documentation for the `HEAD` revision can be found at 40 | the link below. So far, only the beginnings of a developer's guide are 41 | available. Although there is little content, it may help give you an overview 42 | of programming with py-Asterisk. 43 | 44 | * [http://py-asterisk.googlecode.com/hg/doc/GUIDE.html?r=242456f432f2fa2727b26d648bcf7dc502fdcc51 Developer's guide documentation from intro_docs branch] 45 | 46 | 47 | ## Aims 48 | 49 | * Provide a Python 2.3/2.4 package implementing an Asterisk Manager API client (*done*). 50 | * Provide command-line tools to ease day-to-day Asterisk management and debugging (*done*). 51 | * Provide objects for controlling and configuring Asterisk: 52 | * Provide a granular and intuitive event interface (*done*). 53 | * Provide all functionality encapsulated in flexible classes that may be mixed and reused easily (*done*). 54 | * Possibly provide rich data types for manipulation of Asterisk objects, for example '`some_channel.hangup()`' (*done*). 55 | 56 | * Extend the manager API to encapsulate functionality only accessible through the Asterisk console and configuration files (in progress). 57 | * Provide an IAX2 implementation, allowing for voice data to be generated from within Python (protocol bridge, IvR, experimentation, custom apps). 58 | * Provide an enhanced and updated AGI module, and possible provide an EAGI module. 59 | 60 | If you have any ideas for this package, please contact me using the e-mail 61 | address found on the main page of this site. 62 | -------------------------------------------------------------------------------- /THANKS: -------------------------------------------------------------------------------- 1 | This project was originally written by David Wilson, however its present state 2 | is entirely the result of numerous others' toil in keeping it maintained over 3 | the past years. Special thanks to those who reported bugs, and to those who 4 | have contributed to keeping py-Asterisk alive: 5 | 6 | Mister Snilek 7 | Roel van Meer 8 | Rodrigo Ramírez Norambuena 9 | Javier Acosta 10 | Sébastien Couture 11 | Sadiel Orama 12 | Jean Schurger 13 | Alexis de Lattre 14 | Robby Dermody 15 | Mark Purcell 16 | Enrico Zini 17 | Tim Akinbo 18 | Daniel Swarbrick 19 | Bram Vromans 20 | Tzafrir Cohen 21 | César García 22 | James Northcott 23 | Clinton James 24 | Salim Fadhley 25 | Nick Knight 26 | Jacek Ziolkowski 27 | Aiden Andrews-McDermott 28 | "litnimax" (on github) 29 | -------------------------------------------------------------------------------- /asterisk-dump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.3 2 | 3 | ''' 4 | Dump events from the Manager interface to stdout. 5 | ''' 6 | 7 | __author__ = 'David Wilson' 8 | __id__ = '$Id$' 9 | 10 | import sys, time, socket 11 | from Asterisk.Config import Config 12 | from Asterisk.Manager import CoreManager 13 | import Asterisk.Manager, Asterisk.Util 14 | 15 | 16 | 17 | 18 | class MyManager(CoreManager): 19 | ''' 20 | Print events to stdout. 21 | ''' 22 | 23 | def on_Event(self, event): 24 | Asterisk.Util.dump_packet(event) 25 | 26 | 27 | 28 | 29 | def main2(): 30 | manager = MyManager(*Config().get_connection()) 31 | 32 | try: 33 | print '#', repr(manager) 34 | print 35 | manager.serve_forever() 36 | 37 | except KeyboardInterrupt, e: 38 | raise SystemExit 39 | 40 | 41 | 42 | def main(argv): 43 | max_reconnects = 100 44 | reconnect_delay = 2 45 | 46 | while True: 47 | try: 48 | main2() 49 | 50 | except Asterisk.Manager.GoneAwayError, e: 51 | print '#', str(e) 52 | 53 | 54 | except socket.error, e: 55 | print 56 | print '# Connect error:', e[1] 57 | reconnect_delay *= 2 58 | 59 | print '# Waiting', reconnect_delay, 'seconds before reconnect.' 60 | print '# Will try', max_reconnects, 'more times before exit..' 61 | 62 | max_reconnects -= 1 63 | time.sleep(reconnect_delay) 64 | print '# Reconnecting...' 65 | 66 | 67 | 68 | if __name__ == '__main__': 69 | main(sys.argv[1:]) 70 | -------------------------------------------------------------------------------- /asterisk-recover: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.3 2 | 3 | ''' 4 | Recover a channel lost in asterisk. Usage: 5 | 6 | asterisk-recover 7 | 8 | ''' 9 | 10 | __author__ = 'David Wilson' 11 | __id__ = '$Id: asterisk-dump 3 2004-09-03 01:41:42Z dw $' 12 | 13 | import sys 14 | from Asterisk.Config import Config 15 | from Asterisk.Manager import Manager 16 | 17 | 18 | 19 | 20 | def main(argv): 21 | manager = Manager(*Config().get_connection()) 22 | statii = manager.Status() 23 | 24 | if not len(statii): 25 | print >> sys.stderr, 'No channels active.' 26 | print 27 | raise SystemExit, 0 28 | 29 | if len(sys.argv) == 5: 30 | manager.Redirect(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) 31 | 32 | else: 33 | chans = [ x for x in statii.items() if not x[1].has_key('Extension') ] 34 | 35 | for nr, (channel, status) in enumerate(chans): 36 | print 'Channel #%d:' % (nr+1, ), channel 37 | if len(status['CallerID']): 38 | print ' Caller ID:', status['CallerID'] 39 | if status.has_key('Link') and len(status['Link']): 40 | print ' Link:', status['Link'] 41 | 42 | print 43 | 44 | 45 | try: 46 | chan_nr = input('Channel number [default: 1]: ') - 1 47 | except SyntaxError, e: 48 | print 'Using channel #1.' 49 | chan_nr = 1 50 | 51 | if len(chans) < chan_nr: 52 | print 'Using channel #1.' 53 | chan_nr = 0 54 | else: 55 | chan_nr -= 1 56 | 57 | 58 | 59 | context = raw_input('Context [default: local_extensions]: ') or 'local_extensions' 60 | exten = raw_input('Extension [default: 101]: ') or '101' 61 | priority = raw_input('Priority [default: 1]: ') or '1' 62 | 63 | print 'Redirecting', chans[chan_nr][0], 'to', '%s:%s@%s' % (exten, priority, context) 64 | manager.Redirect(chans[chan_nr][0], context, exten, priority) 65 | 66 | 67 | 68 | 69 | if __name__ == '__main__': 70 | main(sys.argv[1:]) 71 | -------------------------------------------------------------------------------- /doc/asterisk-events.txt: -------------------------------------------------------------------------------- 1 | $Id$ 2 | 3 | David Wilson 4 | Events generated by the Asterisk core engine. 5 | 6 | 7 | 8 | 9 | Newexten: 10 | 11 | Event: Newexten 12 | Channel: SIP/101-00c7 13 | Context: macro-ext 14 | Extension: s 15 | Priority: 3 16 | Application: Goto 17 | AppData: s-BUSY 18 | Uniqueid: 1094154321.8 19 | 20 | Event: Newexten 21 | Channel: SIP/101-3f3f 22 | Context: local_extensions 23 | Extension: 917070 24 | Priority: 1 25 | Application: AGI 26 | AppData: /etc/asterisk/agi/ks_doorman_pickup.py|channel_up 27 | Uniqueid: 1094154427.10 28 | 29 | Event: Newexten 30 | Channel: SIP/101-3f3f 31 | Context: local_extensions 32 | Extension: 917070 33 | Priority: 2 34 | Application: Dial 35 | AppData: Zap/G1/17070 36 | Uniqueid: 1094154427.10 37 | 38 | 39 | Hangup: 40 | 41 | Event: Hangup 42 | Channel: SIP/101-3f3f 43 | Uniqueid: 1094154427.10 44 | Cause: 0 45 | 46 | 47 | Newchannel: 48 | 49 | Event: Newchannel 50 | Channel: Zap/2-1 51 | State: Rsrvd 52 | Callerid: 53 | Uniqueid: 1094154427.11 54 | 55 | Event: Newchannel 56 | Channel: SIP/101-3f3f 57 | State: Ring 58 | Callerid: 101 59 | Uniqueid: 1094154427.10 60 | 61 | 62 | Newstate: 63 | 64 | Event: Newstate 65 | Channel: Zap/2-1 66 | State: Dialing 67 | Callerid: 101 68 | Uniqueid: 1094154427.11 69 | 70 | Event: Newstate 71 | Channel: Zap/2-1 72 | State: Up 73 | Callerid: 101 74 | Uniqueid: 1094154427.11 75 | 76 | 77 | Link: 78 | 79 | Event: Link 80 | Channel1: SIP/101-3f3f 81 | Channel2: Zap/2-1 82 | Uniqueid1: 1094154427.10 83 | Uniqueid2: 1094154427.11 84 | 85 | 86 | Unlink: 87 | 88 | Event: Unlink 89 | Channel1: SIP/101-3f3f 90 | Channel2: Zap/2-1 91 | Uniqueid1: 1094154427.10 92 | Uniqueid2: 1094154427.11 93 | 94 | 95 | Reload: 96 | 97 | Event: Reload 98 | Message: Reload Requested 99 | 100 | 101 | Shutdown: 102 | [derived from asterisk.c] 103 | 104 | Event: Shutdown 105 | Shutdown: 106 | Restart: 107 | 108 | 109 | ExtensionStatus: 110 | [derived from manager.c] 111 | 112 | Event: ExtensionStatus 113 | Exten: 114 | Context: 115 | Status: 116 | 117 | 118 | Rename: 119 | [derived from channel.c: channel 'rename' event] 120 | 121 | Event: Rename 122 | Oldname: 123 | Newname: 124 | Uniqueid: 125 | 126 | 127 | Newcallerid: 128 | [derived from channel.c] 129 | 130 | Event: Newcallerid 131 | Channel: 132 | Callerid: 133 | Uniqueid: 134 | 135 | 136 | Alarm: 137 | [derived from chan_zap.c] 138 | 139 | Event: Alarm 140 | Alarm: <(Red|Yellow|Blue|No|Unknown) Alarm|Recovering|Loopback|Not Open|None> 141 | Channel: 142 | 143 | 144 | AlarmClear: 145 | [derived from chan_zap.c] 146 | 147 | Event: AlarmClear 148 | Channel: 149 | 150 | 151 | Agentcallbacklogoff: 152 | [derived from chan_agent.c] 153 | 154 | Event: Agentcallbacklogoff 155 | Agent: 156 | Loginchan: 157 | Logintime: 158 | Reason: Autologoff 159 | Uniqueid: 160 | 161 | Event: Agentcallbacklogoff 162 | Agent: 163 | Loginchan: 164 | Logintime: 165 | Uniqueid: 166 | 167 | 168 | Agentcallbacklogin: 169 | [derived from chan_agent.c] 170 | 171 | Event: Agentcallbacklogin 172 | Agent: 173 | Loginchan: 174 | Uniqueid: 175 | 176 | 177 | Agentlogin: 178 | [derived from chan_agent.c] 179 | 180 | Event: Agentlogin 181 | Agent: 182 | Channel: 183 | Uniqueid: 184 | 185 | 186 | Agentlogoff: 187 | [derived from chan_agent.c] 188 | 189 | Event: Agentlogoff 190 | Agent: 191 | Logintime: 192 | Uniqueid: 193 | 194 | 195 | MeetmeJoin: 196 | [derived from app_meetme.c] 197 | 198 | Event: MeetmeJoin 199 | Channel: 200 | Uniqueid: 201 | Meetme: 202 | Usernum: 203 | 204 | 205 | MeetmeLeave: 206 | [derived from app_meetme.c] 207 | 208 | Event: MeetmeLeave 209 | Channel: 210 | Uniqueid: 211 | Meetme: 212 | Usernum: 213 | 214 | 215 | MessageWaiting: 216 | [derived from app_voicemail.c] 217 | 218 | Event: MessageWaiting 219 | Mailbox: @ 220 | Waiting: 221 | 222 | Event: MessageWaiting 223 | Mailbox: 224 | Waiting: 225 | 226 | 227 | [UserEvent]: 228 | [derived from app_userevent.c] 229 | 230 | Event: 231 | Channel: 232 | Uniqueid: 233 | 234 | Event: 235 | Channel: 236 | Uniqueid: 237 | 238 | 239 | 240 | Join: 241 | [derived from app_queue.c] 242 | 243 | Event: join 244 | Channel: 245 | CallerID: 246 | Queue: 247 | Position: 248 | Count: 249 | 250 | 251 | Leave: 252 | [derived from app_queue.c] 253 | 254 | Event: leave 255 | Channel: 256 | Queue: 257 | Count: 258 | 259 | 260 | AgentCalled: 261 | [derived from app_queue.c] 262 | 263 | Event: AgentCalled 264 | AgentCalled: 265 | ChannelCalling: 266 | CallerID: 267 | Context: 268 | Extension: 269 | Priority: 270 | 271 | 272 | ParkedCall: 273 | [derived from res_features.c] 274 | 275 | Event: ParkedCall 276 | Exten: 277 | Channel: 278 | From: 279 | Timeout: 280 | CallerID: 281 | 282 | 283 | Cdr: 284 | [derived from cdr_manager.c] 285 | 286 | Event: Cdr 287 | AccountCode: 288 | Source: 289 | Destination: 290 | DestinationContext: 291 | CallerID: 292 | Channel: 293 | DestinationChannel: 294 | LastApplication: 295 | LastData: 296 | StartTime: 297 | AnswerTime: 298 | EndTime: 299 | Duration: 300 | BillableSeconds: 301 | Disposition: 302 | AMAFlags: 303 | UniqueID: 304 | UserField: 305 | 306 | 307 | ParkedCallsComplete: 308 | [sent following an Action: ParkedCalls] 309 | 310 | Event: ParkedCallsComplete 311 | 312 | 313 | QueueParams: 314 | [sent following an Action: Queues] 315 | 316 | Event: QueueParams 317 | Queue: sales 318 | Max: 0 319 | Calls: 0 320 | Holdtime: 0 321 | Completed: 0 322 | Abandoned: 0 323 | ServiceLevel: 0 324 | ServicelevelPerf: 0.0 325 | 326 | 327 | QueueMember: 328 | [sent following an Action: Queues if a queue has members] 329 | 330 | Event: QueueMember 331 | Queue: sales 332 | Location: SIP/101 333 | Membership: dynamic 334 | Penalty: 0 335 | CallsTaken: 0 336 | LastCall: 0 337 | 338 | 339 | QueueStatusEnd: 340 | [sent following an Action: Queues to signify end of output] 341 | 342 | Event: QueueStatusEnd 343 | 344 | 345 | Status: 346 | 347 | Event: Status 348 | Channel: Zap/2-1 349 | CallerID: 101 350 | Account: 351 | State: Up 352 | Link: SIP/101-5cf0 353 | Uniqueid: 1094166088.26 354 | 355 | Event: Status 356 | Channel: SIP/101-5cf0 357 | CallerID: 101 358 | Account: 359 | State: Up 360 | Context: local_extensions 361 | Extension: 917070 362 | Priority: 2 363 | Seconds: 11 364 | Link: Zap/2-1 365 | Uniqueid: 1094166088.25 366 | 367 | 368 | StatusComplete: 369 | [sent on end of Status events after Action: status] 370 | 371 | Event: StatusComplete 372 | 373 | 374 | ZapShowChannels: 375 | [sent on Action: ZapShowChannels] 376 | 377 | Event: ZapShowChannels 378 | Channel: 2 379 | Signalling: FXS Kewlstart 380 | Context: pstn_menu 381 | Alarm: No Alarm 382 | 383 | 384 | ZapShowChannelsComplete: 385 | [send on Action: ZapShowChannels end] 386 | 387 | Event: ZapShowChannelsComplete 388 | -------------------------------------------------------------------------------- /doc/event-handling.txt: -------------------------------------------------------------------------------- 1 | Event Handlers in py-Asterisk. 2 | $Id$ 3 | 4 | 5 | 6 | Overview 7 | 8 | In order to allow for much more flexible event handling, and better 9 | grouping of functionality within software utilising py-asterisk, it was 10 | decided that subclassing of the Manager/BaseManager object was no longer 11 | appropriate for adding event handlers. 12 | 13 | Reasons: 14 | 15 | - It doesn't make sense. Event handlers do not extend the Manager object 16 | in any way, they are clients of the object, and not extensions of the 17 | object. 18 | 19 | 20 | - Subclasses of the Manager object should be used to add new 21 | functionality, for example, to add in-house custom manager actions. 22 | 23 | 24 | - Splitting event handling off away from the Manager object, combined with 25 | the newer rich BaseChannel type, opens the package to greater 26 | flexibility in dealing with multiple PBXes. 27 | 28 | Simply define your class of event handlers, which expect Event, Channel, 29 | and Manager objects passed to them, then register the event handlers 30 | with multiple Manager objects. Imagine a monitoring application 31 | responsible for a network of PBXes, and call monitoring and logging on 32 | each. 33 | 34 | 35 | - Allowing multiple handlers to be specified for each event allows a whole 36 | new range of exciting mix-ins to be created, in-line with the vision set 37 | out for py-asterisk originally: live Python objects that accurately 38 | represent the state of equivalent Asterisk objects. 39 | 40 | For instance, it is concievable that a mix-in may be created that keeps 41 | track of BaseChannel objects created, that registers handlers for 42 | channel state change events, and that automatically updates any created 43 | channel objects with live state information. 44 | 45 | 46 | - Allowing multiple handlers allows solving of the sync/async problem in a 47 | number of different ways. Rather than forcing the py-asterisk user to 48 | choose between either synchronous or asyncronous behaviour, a 49 | BaseManager object may continue running asyncronously while a 50 | synchronous command is executing. 51 | 52 | This is possible at present anyway, but we can now do it without 53 | disrupting any client code that may be listening for the same events. 54 | This offers maximum flexibility in programming, and may, for instance, 55 | allow an advanced programmer to work on asynchronous code while his 56 | trainee can get by with simple method calls. 57 | 58 | 59 | - It is possible (either already or otherwise) to implement generators 60 | that provide event feedback as it happens. Imagine: 61 | 62 | for channel in manager.iter_channels(): 63 | print "just got info on", channel 64 | 65 | 66 | 67 | 68 | Concept 69 | 70 | Taken from C# and other languages, a source of events no longer is a stub 71 | function that you override, but a collection object that allows multiple 72 | callables to be registered for one event. Those callables may be 73 | completely unrelated to the class originating the event. 74 | 75 | Through the use of BaseChannel, Event, and exception types, associations 76 | between event, channel, and error information is created and accessible 77 | through references in the object you are currently dealing with. 78 | 79 | When you define your group of event handling functions, you may now easily 80 | put them within the same class, possibly a class directly associated with 81 | your application. Functionality to automatically register your event 82 | handlers with a specific Manager class can be provided through an optional 83 | mix-in, or via your own code if you have custom logic for deciding where 84 | (and how many times) your event handlers are registered. 85 | 86 | 87 | 88 | 89 | Objects 90 | 91 | BaseManager (and associated mix-ins) 92 | 93 | This class represents a single connection to an Asterisk instance. It 94 | provides methods for executing commands on the Asterisk PBX and also 95 | acts as a source of events. 96 | 97 | The BaseManager class itself provides one stub function for the 98 | reception of events, on_Event(). By overriding on_Event, subclasses 99 | may decide how events should be dispatched. 100 | 101 | 102 | BaseChannel (and associated subclasses) 103 | 104 | This class represents a single active channel object, and maintains a 105 | reference to it's associated BaseManager object. 106 | 107 | 108 | EventRegistrar 109 | 110 | This mix-in class provides a class variable named , a method 111 | for registering the contents of with a BaseManager 112 | instance, and an overrideable <__init__> which automatically calls the 113 | registration method. 114 | 115 | This can act as a base class for grouping event handlers together that 116 | may be used with multiple PBXes, or as a simple mix-in for your own 117 | class heiarchy that provides an automated way of register event 118 | handlers. 119 | 120 | Example 1: 121 | 122 | class MyEvents(Asterisk.Manager.EventRegistrar): 123 | def on_Status(self, event): 124 | ... 125 | 126 | handlers += on_Status 127 | 128 | 129 | def on_StatusComplete(self, event): 130 | ... 131 | 132 | handlers += on_StatusComplete 133 | 134 | 135 | pbx = Manager(...) 136 | handlers = MyEvents(pbx) 137 | 138 | 139 | Example 2: 140 | 141 | class MyEvents2(EventRegistrar, SomeBaseClass): 142 | ... 143 | 144 | pbx = Manager(...) 145 | handlers = MyEvents(["your", "own", "__init__", "args"]) 146 | handlers.register_manager(pbx) 147 | 148 | -------------------------------------------------------------------------------- /doc/license.txt: -------------------------------------------------------------------------------- 1 | 2 | The py-Asterisk License 3 | ----------------------- 4 | 5 | This is the MIT license with the trailing paragraph reformatted for 6 | readability. If this license is not compatible with your usage of the 7 | software, please do not hesitate to contact me as the situation is easy to 8 | resolve. 9 | 10 | 11 | Copyright 2004, David M. Wilson. 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a 14 | copy of this software and associated documentation files (the "Software"), 15 | to deal in the Software without restriction, including without limitation 16 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 17 | and/or sell copies of the Software, and to permit persons to whom the 18 | Software is furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | The software is provided "as is", without warranty of any kind, express or 24 | implied, including but not limited to the warranties of merchantability, 25 | fitness for a particular purpose and non-infringement. In no event shall 26 | the authors or copyright holders be liable for any claim, damages or other 27 | liability, whether in an action of contract, tort or otherwise, arising 28 | from, out of or in connection with the software or the use or other 29 | dealings in the software. 30 | -------------------------------------------------------------------------------- /doc/py-asterisk.conf.sample: -------------------------------------------------------------------------------- 1 | ; vim: syntax=dosini 2 | ; $Id$ 3 | 4 | ; py-Asterisk sample configuration file. 5 | 6 | 7 | [py-asterisk] 8 | default connection=my-pbx 9 | 10 | 11 | [connection: my-pbx] 12 | hostname=pbx.lan.yourcompany.net 13 | port=5038 14 | username=admin 15 | secret=letmein 16 | -------------------------------------------------------------------------------- /doc/readme.txt: -------------------------------------------------------------------------------- 1 | $Id$ 2 | 3 | 4 | Asterisk Manager API interface module for Python. 5 | 6 | This module provides an object oriented interface to the Asterisk Manager API, 7 | whilest embracing some applicable Python concepts: 8 | 9 | - Functionality is split into seperate mix-in classes. 10 | 11 | - Asterisk PBX errors cause fairly granular exceptions. 12 | 13 | - Docstrings are provided for all objects. 14 | 15 | - Asterisk data is translated into data stored using Python types, so 16 | working with it should be trivial. Through the use of XMLRPCServer or 17 | similar, it should be trivial to expose this conversion to other 18 | languages. 19 | 20 | Using the Manager or CoreManager objects, or using your own object with the 21 | CoreActions mix-in, you may simply call methods of the instanciated object and 22 | they will block until all data is available. 23 | 24 | Support for asynchronous usage is incomplete; consider running the client from 25 | a thread and driven through a queue when integrating with an asynchronous 26 | design. 27 | -------------------------------------------------------------------------------- /doc/todo.txt: -------------------------------------------------------------------------------- 1 | $Id$ 2 | 3 | Todo List For py-Asterisk. 4 | 5 | - Import old public domain AGI.py or rewrite it. At the least refactor it 6 | to not be so terrible. 7 | 8 | - Status/ParkedCalls/ShowZapChannels/etc. can only be used asyncronously, 9 | they cannot be used to simply trigger sending of events. This is wrong, 10 | but how do I fix? 11 | 12 | - Automatic state tracking of some sort for richly typed object 13 | representations - ie. automatically Channel.state from the Manager 14 | object so that channels may be passed around irrelevant of their parent 15 | Manager object, and may be used without raising exceptions (for eg. when 16 | a channel no longer exists). 17 | 18 | - Go through and make stuff asynchronous again. 19 | 20 | - Create proper documentation. 21 | -------------------------------------------------------------------------------- /examples/grab-callerid.txt: -------------------------------------------------------------------------------- 1 | On Tue, Oct 12, 2004 at 08:30:49PM +0100, Nick Knight wrote: 2 | 3 | > BTW, I have figured out call origination - the other thing I am looking 4 | > at is caller ID parsing - have you got any example code to monitor and 5 | > manage events for inbound calls. 6 | 7 | Hi Nick! 8 | 9 | When you talk about CallerID parsing, do you mean breaking the CallerID 10 | up into , , ? 11 | 12 | Quick example for grabbing CallerID: 13 | 14 | 15 | class GrabCallerID(object): 16 | ''' 17 | Subscribe to 'Newchannel' events, and grab the CallerID of new 18 | channels as they appear. 19 | ''' 20 | 21 | def Newchannel(self, pbx, event): 22 | ''' 23 | Event handler for the Asterisk 'Newchannel' event. 24 | ''' 25 | 26 | cli = event.Callerid 27 | print "CallerID for %s is: %s" % (event.Channel, cli) 28 | 29 | 30 | if self.bad_caller(cli): 31 | event.Channel.hangup() 32 | 33 | 34 | def bad_caller(self, cli): 35 | ''' 36 | Return truth if we don't like this caller. 37 | ''' 38 | 39 | return cli.startswith('0870') # or whatever. 40 | 41 | 42 | def __init__(self): 43 | # Create an EventCollection instance, which is a mapping of 44 | # event name <-> list of delegates that you can merge with 45 | # the EventCollections of an Asterisk.Manager.BaseManager 46 | # (or derivitaves) instance. 47 | 48 | # Confused? I thought as much. Give me a few weeks and 49 | # you'll have some introductary documentation to go by. :) 50 | 51 | self.events = events = Asterisk.Util.EventCollection() 52 | 53 | # Add our event handler. 54 | events.subscribe('Newchannel', self.Newchannel) 55 | 56 | 57 | def register(self, some_pbx): 58 | ''' 59 | Register our list of events with 's 60 | EventCollection. 61 | ''' 62 | 63 | some_pbx.events += events 64 | 65 | 66 | def unregister(self, some_pbx): 67 | ''' 68 | Unregister our events from . 69 | ''' 70 | 71 | some_pbx.events -= events 72 | 73 | 74 | 75 | 76 | Now all you do is something like: 77 | 78 | from Asterisk.Manager import Manager 79 | 80 | pbx = Manager(('localhost', 5038), 'dw', 'letmein') 81 | grab = GrabCallerID() 82 | 83 | grab.register(pbx) 84 | 85 | # Sit receiving events in an endless loop, Newchannel events 86 | # will end up in the GrabCallerID.Newchannel method. 87 | pbx.serve_forever() 88 | 89 | 90 | For playing around, you may also be interested in this snippet: 91 | 92 | import logging 93 | logging.basicConfig() 94 | logging.getLogger().setLevel(logging.DEBUG) 95 | 96 | This will cause a tonne of debug output to be printed to your 97 | terminal. Read the logging package's documentation for more 98 | information on redirecting this output to a file. 99 | 100 | If you import Asterisk.Manager first, then you can also set a 101 | further two levels: logging.PACKET and logging.IO which cause 102 | even greater output to be generated. 103 | 104 | 105 | For debugging, there is also Asterisk.Util.dump_human which dumps 106 | packets in a half-readable format. The 'asterisk-dump' command-line 107 | utility is also useful for working out what is going wrong. 108 | 109 | 110 | Hope this helps! 111 | 112 | 113 | David. 114 | -- 115 | 20:26 you're an emotional rollercoaster 116 | 20:26 THAT ONLY GOES DOWN 117 | -------------------------------------------------------------------------------- /py-asterisk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.3 2 | 3 | ''' 4 | py-asterisk: User interface to Asterisk.CLI. 5 | ''' 6 | 7 | __author__ = 'David Wilson' 8 | __id__ = '$Id$' 9 | 10 | import Asterisk, Asterisk.Manager, Asterisk.CLI 11 | import sys, os, getopt 12 | 13 | 14 | 15 | 16 | progname = os.path.basename(sys.argv[0]) 17 | 18 | try: 19 | sys.exit(Asterisk.CLI.command_line(sys.argv)) 20 | 21 | except Asterisk.CLI.ArgumentsError, error: 22 | print >> sys.stderr, progname, 'error:', str(error) 23 | Asterisk.CLI.usage(progname, sys.stderr) 24 | 25 | except Asterisk.BaseException, error: 26 | print >> sys.stderr, progname, 'error:', str(error) 27 | 28 | except IOError, error: 29 | print >> sys.stderr, '%s: %s: %s' %\ 30 | ( progname, error.filename, error.strerror ) 31 | 32 | except getopt.GetoptError, error: 33 | print >> sys.stderr, '%s: %s' %\ 34 | ( progname, error.msg ) 35 | 36 | Asterisk.CLI.usage(progname, sys.stderr) 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | py-Asterisk distutils script. 5 | ''' 6 | 7 | from setuptools import setup 8 | 9 | __author__ = 'David Wilson' 10 | __id__ = '$Id$' 11 | 12 | 13 | setup(name='py-Asterisk', 14 | version='0.5.18', 15 | description='Asterisk Manager API Python interface.', 16 | author='David Wilson', 17 | author_email='dw@botanicus.net', 18 | license='MIT', 19 | url='https://github.com/jeansch/py-asterisk/', 20 | packages=['Asterisk'], 21 | scripts=['asterisk-dump', 'py-asterisk']) 22 | --------------------------------------------------------------------------------