| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787 |
- #!/usr/bin/env python3
- import asyncio
- import aiohttp
- import logging
- import os
- import re
- import json
- from datetime import datetime as dt
- from datetime import timedelta as td
- from typing import Any, Optional
- from databases import Database
- from quart import jsonify, request, render_template_string, abort
- from quart_openapi import Pint, Resource
- from http import HTTPStatus
- from panoramisk import Manager, Message
- from utils import *
- from logging.config import dictConfig
- class PintDB:
- def __init__(self, app: Optional[Pint] = None) -> None:
- self.init_app(app)
- self._db = Database(app.config["DB_URI"])
- def init_app(self, app: Pint) -> None:
- app.before_serving(self._before_serving)
- app.after_serving(self._after_serving)
- async def _before_serving(self) -> None:
- await self._db.connect()
- async def _after_serving(self) -> None:
- await self._db.disconnect()
- def __getattr__(self, name: str) -> Any:
- return getattr(self._db, name)
- # One asyncio event loop is used for AMI communication and HTTP requests routing with Quart
- main_loop = asyncio.get_event_loop()
- app = Pint(__name__, title=os.getenv('APP_TITLE', 'PBX API'), no_openapi=True)
- app.config.update({
- 'TITLE': os.getenv('APP_TITLE', 'PBX API'),
- 'APPLICATION_ROOT': os.getenv('APP_APPLICATION_ROOT', None),
- 'SCHEME': os.getenv('APP_SCHEME', 'http'),
- 'FQDN': os.getenv('APP_FQDN', '127.0.0.1'),
- 'PORT': int(os.getenv('APP_API_PORT', 8000)),
- 'BODY_TIMEOUT': int(os.getenv('APP_BODY_TIMEOUT', 60)),
- 'DEBUG': os.getenv('APP_DEBUG', 'False').lower() in TRUEs,
- 'MAX_CONTENT_LENGTH': int(os.getenv('APP_MAX_CONTENT_LENGTH', 16777216)),
- 'AMI_HOST': os.getenv('APP_AMI_HOST', '127.0.0.1'),
- 'AMI_PORT': int(os.getenv('APP_AMI_PORT', 5038)),
- 'AMI_USERNAME': os.getenv('APP_AMI_USERNAME', 'app'),
- 'AMI_SECRET': os.getenv('APP_AMI_SECRET', 'secret'),
- 'AMI_PING_DELAY': int(os.getenv('APP_AMI_PING_DELAY', 10)),
- 'AMI_PING_INTERVAL': int(os.getenv('APP_AMI_PING_INTERVAL', 10)),
- 'AMI_TIMEOUT': int(os.getenv('APP_AMI_TIMEOUT', 5)),
- 'AUTH_HEADER': os.getenv('APP_AUTH_HEADER', 'APP-auth-token'),
- 'AUTH_SECRET': os.getenv('APP_AUTH_SECRET', '3bfbeaabf363dd64fe263bd36830a6b6'),
- 'SWAGGER_JS_URL': os.getenv('APP_SWAGGER_JS_URL', SWAGGER_JS_URL),
- 'SWAGGER_CSS_URL': os.getenv('APP_SWAGGER_CSS_URL', SWAGGER_CSS_URL),
- 'STATE_CALLBACK_URL': os.getenv('APP_STATE_CALLBACK_URL', None),
- 'DB_URI': 'mysql://{}:{}@{}:{}/{}'.format(os.getenv('MYSQL_USER', 'asterisk'),
- os.getenv('MYSQL_PASSWORD', 'secret'),
- os.getenv('MYSQL_SERVER', 'db'),
- os.getenv('APP_PORT_MYSQL', '3306'),
- os.getenv('FREEPBX_CDRDBNAME', None)),
- 'EXTRA_API_URL': os.getenv('APP_EXTRA_API_URL', None),
- 'STATE_CACHE': {'user':{},
- 'presence':{}}})
- manager = Manager(
- loop=main_loop,
- host=app.config['AMI_HOST'],
- port=app.config['AMI_PORT'],
- username=app.config['AMI_USERNAME'],
- secret=app.config['AMI_SECRET'],
- ping_delay=app.config['AMI_PING_DELAY'],
- ping_interval=app.config['AMI_PING_INTERVAL'],
- reconnect_timeout=app.config['AMI_TIMEOUT'],
- )
- class AuthMiddleware:
- '''ASGI process middleware that rejects requests missing
- the correct authentication header'''
- def __init__(self, app):
- self.app = app
- async def __call__(self, scope, receive, send):
- if 'headers' not in scope:
- return await self.app(scope, receive, send)
- for header, value in scope['headers']:
- if ((header == bytes(app.config['AUTH_HEADER'].lower(), 'utf-8')) and
- (value == bytes(app.config['AUTH_SECRET'], 'utf-8'))):
- return await self.app(scope, receive, send)
- # Paths "/openapi.json" and "/ui" do not require auth
- if (('path' in scope) and
- (scope['path'] in NO_AUTH_ROUTES)):
- return await self.app(scope, receive, send)
- return await self.error_response(receive, send)
- async def error_response(self, receive, send):
- await send({'type': 'http.response.start',
- 'status': 401,
- 'headers': [(b'content-length', b'21')]})
- await send({'type': 'http.response.body',
- 'body': b'Authorization requred',
- 'more_body': False})
- app.asgi_app = AuthMiddleware(app.asgi_app)
- db = PintDB(app)
- @manager.register_event('FullyBooted')
- async def fullyBootedCallback(mngr: Manager, msg: Message):
- await refreshStatesCache()
- @manager.register_event('ExtensionStatus')
- async def extensionStatusCallback(mngr: Manager, msg: Message):
- user = msg.exten #hint = msg.hint
- state = msg.statustext.lower()
- if user in app.config['STATE_CACHE']['user']:
- prevState = getUserStateCombined(user)
- app.config['STATE_CACHE']['user'][user] = state
- combinedState = getUserStateCombined(user)
- if combinedState != prevState:
- await userStateChangeCallback(user, combinedState, prevState)
- @manager.register_event('PresenceStatus')
- async def presenceStatusCallback(mngr: Manager, msg: Message):
- user = msg.exten #hint = msg.hint
- state = msg.status.lower()
- if user in app.config['STATE_CACHE']['user']:
- prevState = getUserStateCombined(user)
- app.config['STATE_CACHE']['presence'][user] = state
- combinedState = getUserStateCombined(user)
- if combinedState != prevState:
- await userStateChangeCallback(user, combinedState, prevState)
- async def getCDR(start=None, end=None, **kwargs):
- cdr = {}
- if end is None:
- end = dt.now()
- if start is None:
- start=(end - td(hours=24))
- async for row in db.iterate(query='''SELECT linkedid,
- uniqueid,
- calldate,
- did,
- src,
- dst,
- clid,
- dcontext,
- channel,
- dstchannel,
- lastapp,
- duration,
- billsec,
- disposition,
- recordingfile,
- cnum,
- cnam,
- outbound_cnum,
- outbound_cnam,
- dst_cnam,
- peeraccount
- FROM cdr
- WHERE calldate
- BETWEEN :start AND :end
- ORDER BY linkedid,
- calldate,
- uniqueid;''',
- values={'start':start,
- 'end':end}):
- call = {_k: str(_v) for _k, _v in row.items() if _k != 'linkedid' and _v != ''}
- cdr.setdefault(row['linkedid'],[]).append(call)
- return cdr
- async def getCEL(start=None, end=None, **kwargs):
- cel = {}
- if end is None:
- end = dt.now()
- if start is None:
- start=(end - td(hours=24))
- async for row in db.iterate(query='''SELECT linkedid,
- uniqueid,
- eventtime,
- eventtype,
- cid_name,
- cid_num,
- cid_ani,
- cid_rdnis,
- cid_dnid,
- exten,
- context,
- channame,
- appname,
- uniqueid,
- linkedid
- FROM cel
- WHERE eventtime
- BETWEEN :start AND :end
- ORDER BY linkedid,
- uniqueid,
- eventtime;''',
- values={'start':start,
- 'end':end}):
- event = {_k: str(_v) for _k, _v in row.items() if _k != 'linkedid' and _v != ''}
- cel.setdefault(row['linkedid'],[]).append(event)
- return cel
- @app.before_first_request
- async def initHttpClient():
- app.config['HTTP_CLIENT'] = aiohttp.ClientSession(loop=main_loop)
- @app.route('/openapi.json')
- async def openapi():
- '''Generates JSON that conforms OpenAPI Specification
- '''
- schema = app.__schema__
- schema['servers'] = [{'url':'{}://{}:{}'.format(app.config['SCHEME'],
- app.config['FQDN'],
- app.config['PORT'])}]
- if app.config['EXTRA_API_URL'] is not None:
- schema['servers'].append({'url':app.config['EXTRA_API_URL']})
- schema['components'] = {'securitySchemes':{'ApiKey':{'type': 'apiKey',
- 'name': app.config['AUTH_HEADER'],
- 'in': 'header'}}}
- schema['security'] = [{'ApiKey':[]}]
- return jsonify(schema)
- @app.route('/ui')
- async def ui():
- '''Swagger UI
- '''
- return await render_template_string(SWAGGER_TEMPLATE,
- title=app.config['TITLE'],
- js_url=app.config['SWAGGER_JS_URL'],
- css_url=app.config['SWAGGER_CSS_URL'])
- @app.route('/ami/action', methods=['POST'])
- async def action():
- _payload = await request.get_data()
- reply = await manager.send_action(json.loads(_payload))
- return str(reply)
- @app.route('/ami/getvar/<string:variable>')
- async def amiGetVar(variable):
- '''AMI GetVar
- Returns value of requested variable using AMI action GetVar in background.
- Parameters:
- variable (string): Variable to query for
- Returns:
- string: Variable value or empty string if variable not found
- '''
- reply = await manager.send_action({'Action': 'GetVar',
- 'Variable': variable})
- app.logger.warning('GetVar({})->{}'.format(variable, reply.value))
- return reply.value
- @app.route('/ami/auths')
- async def amiPJSIPShowAuths():
- auths = {}
- reply = await manager.send_action({'Action':'PJSIPShowAuths'})
- if len(reply) >= 2:
- for message in reply:
- if ((message.event == 'AuthList') and
- ('objecttype' in message) and
- (message.objecttype == 'auth')):
- auths[message.username] = message.password
- return successReply(auths)
- @app.route('/ami/aors')
- async def amiPJSIPShowAors():
- aors = {}
- reply = await manager.send_action({'Action':'PJSIPShowAors'})
- if len(reply) >= 2:
- for message in reply:
- if ((message.event == 'AorList') and
- ('objecttype' in message) and
- (message.objecttype == 'aor')):
- aors[message.objectname] = message.contacts
- return successReply(aors)
- async def amiSetVar(variable, value):
- '''AMI SetVar
- Sets variable using AMI action SetVar to value in background.
- Parameters:
- variable (string): Variable to set
- value (string): Value to set for variable
- Returns:
- string: None if SetVar was successfull, error message overwise
- '''
- reply = await manager.send_action({'Action': 'SetVar',
- 'Variable': variable,
- 'Value': value})
- app.logger.warning('SetVar({}, {})'.format(variable, value))
- if isinstance(reply, Message):
- if reply.success:
- return None
- else:
- return reply.message
- return 'AMI error'
- async def amiDBGet(family, key):
- '''AMI DBGet
- Returns value of requested astdb key using AMI action DBGet in background.
- Parameters:
- family (string): astdb key family to query for
- key (string): astdb key to query for
- Returns:
- string: Value or empty string if variable not found
- '''
- reply = await manager.send_action({'Action': 'DBGet',
- 'Family': family,
- 'Key': key})
- if (isinstance(reply, list) and
- (len(reply) > 1)):
- for message in reply:
- if (message.event == 'DBGetResponse'):
- app.logger.warning('DBGet(/{}/{})->{}'.format(family, key, message.val))
- return message.val
- app.logger.warning('DBGet(/{}/{})->Error!'.format(family, key))
- return None
- async def amiDBPut(family, key, value):
- '''AMI DBPut
- Writes value to astdb by family and key using AMI action DBPut in background.
- Parameters:
- family (string): astdb key family to write to
- key (string): astdb key to write to
- value (string): value to write
- Returns:
- boolean: True if DBPut action was successfull, False overwise
- '''
- reply = await manager.send_action({'Action': 'DBPut',
- 'Family': family,
- 'Key': key,
- 'Val': value})
- app.logger.warning('DBPut(/{}/{}, {})'.format(family, key, value))
- if (isinstance(reply, Message) and reply.success):
- return True
- return False
- async def amiDBDel(family, key):
- '''AMI DBDel
- Deletes key from family in astdb using AMI action DBDel in background.
- Parameters:
- family (string): astdb key family
- key (string): astdb key to delete
- Returns:
- boolean: True if DBDel action was successfull, False overwise
- '''
- reply = await manager.send_action({'Action': 'DBDel',
- 'Family': family,
- 'Key': key})
- app.logger.warning('DBDel(/{}/{})'.format(family, key))
- if (isinstance(reply, Message) and reply.success):
- return True
- return False
- async def amiSetHint(context, user, hint):
- '''AMI SetHint
- Sets hint for user in context using AMI action DialplanUserAdd with Replace=true in background.
- Parameters:
- context (string): dialplan context
- user (string): user
- hint (string): hint for user
- Returns:
- boolean: True if DialplanUserAdd action was successfull, False overwise
- '''
- reply = await manager.send_action({'Action': 'DialplanExtensionAdd',
- 'Context': context,
- 'Extension': user,
- 'Priority': 'hint',
- 'Application': hint,
- 'Replace': 'yes'})
- app.logger.warning('SetHint({},{},{})'.format(context, user, hint))
- if (isinstance(reply, Message) and reply.success):
- return True
- return False
- async def amiPresenceState(user):
- '''AMI PresenceState request for CustomPresence provider
- Parameters:
- user (string): user
- Returns:
- boolean, string: True and state or False and error message
- '''
- reply = await manager.send_action({'Action': 'PresenceState',
- 'Provider': 'CustomPresence:{}'.format(user)})
- app.logger.warning('PresenceState({})'.format(user))
- if isinstance(reply, Message):
- if reply.success:
- return True, reply.state
- else:
- return False, reply.message
- return False, 'AMI error'
- async def amiPresenceStateList():
- states = {}
- reply = await manager.send_action({'Action':'PresenceStateList'})
- if len(reply) >= 2:
- for message in reply:
- if message.event == 'PresenceStateChange':
- user = re.search('CustomPresence:(\d+)', message.presentity).group(1)
- states[user] = message.status
- app.logger.warning('PresenceStateList: {}'.format(','.join(states.keys())))
- return states
- async def amiExtensionStateList():
- states = {}
- reply = await manager.send_action({'Action':'ExtensionStateList'})
- if len(reply) >= 2:
- for message in reply:
- if ((message.event == 'ExtensionStatus') and
- (message.context == 'ext-local')):
- states[message.exten] = message.statustext.lower()
- app.logger.warning('ExtensionStateList: {}'.format(','.join(states.keys())))
- return states
- async def amiCommand(command):
- '''AMI Command
- Runs specified command using AMI action Command in background.
- Parameters:
- command (string): command to run
- Returns:
- boolean, list: tuple representing the boolean result of request and list of lines of command output
- '''
- reply = await manager.send_action({'Action': 'Command',
- 'Command': command})
- result = []
- if (isinstance(reply, Message) and reply.success):
- if isinstance(reply.output, list):
- result = reply.output
- else:
- result = reply.output.split('\n')
- app.logger.warning('Command({})->{}'.format(command, '\n'.join(result)))
- return True, result
- app.logger.warning('Command({})->Error!'.format(command))
- return False, result
- async def amiReload(module='core'):
- '''AMI Reload
- Reload specified asterisk module using AMI action reload in background.
- Parameters:
- module (string): module to reload, defaults to core
- Returns:
- boolean: True if Reload action was successfull, False overwise
- '''
- reply = await manager.send_action({'Action': 'Reload',
- 'Module': module})
- app.logger.warning('Reload({})'.format(module))
- if (isinstance(reply, Message) and reply.success):
- return True
- return False
- async def getGlobalVars():
- globalVars = GlobalVars()
- for _var in globalVars.d():
- setattr(globalVars, _var, await amiGetVar(_var))
- return globalVars
- async def setUserHint(user, dial, ast):
- if dial in NONEs:
- hint = 'CustomPresence:{}'.format(user)
- else:
- _dial= [dial]
- if (ast.DNDDEVSTATE == 'TRUE'):
- _dial.append('Custom:DND{}'.format(user))
- hint = '{},CustomPresence:{}'.format('&'.join(_dial), user)
- return await amiSetHint('ext-local', user, hint)
- async def amiQueues():
- queues = {}
- reply = await manager.send_action({'Action':'QueueStatus'})
- if len(reply) >= 2:
- for message in reply:
- if message.event == 'QueueMember':
- _qm = QueueMember(re.search('Local\/(\d+)', message.location).group(1))
- queues.setdefault(message.queue, []).append(_qm.fromMessage(message))
- return queues
- async def amiDeviceChannel(device):
- reply = await manager.send_action({'Action':'CoreShowChannels'})
- if len(reply) >= 2:
- for message in reply:
- if message.event == 'CoreShowChannel':
- if message.calleridnum == device:
- return message.channel
- return None
- async def setQueueStates(queues, user, device, state):
- for queue in [_q for _q, _ma in queues.items() for _m in _ma if _m.user == user]:
- await amiSetVar('DEVICE_STATE(Custom:QUEUE{}*{})'.format(device, queue), state)
- async def getDeviceUser(device):
- return await amiDBGet('DEVICE', '{}/user'.format(device))
- async def getDeviceDial(device):
- return await amiDBGet('DEVICE', '{}/dial'.format(device))
- async def getUserCID(user):
- return await amiDBGet('AMPUSER', '{}/cidnum'.format(user))
- async def setDeviceUser(device, user):
- return await amiDBPut('DEVICE', '{}/user'.format(device), user)
- async def getUserDevice(user):
- return await amiDBGet('AMPUSER', '{}/device'.format(user))
- async def setUserDevice(user, device):
- if device is None:
- return await amiDBDel('AMPUSER', '{}/device'.format(user))
- else:
- return await amiDBPut('AMPUSER', '{}/device'.format(user), device)
- async def unbindOtherDevices(user, newDevice, queues, ast):
- '''Unbinds user from all devices except newDevice and sets
- all required device states.
- '''
- devices = await amiDBGet('AMPUSER', '{}/device'.format(user))
- if devices not in NONEs:
- for _device in sorted(set(devices.split('&')), key=int):
- if _device != newDevice:
- if ast.FMDEVSTATE == 'TRUE':
- await amiSetVar('DEVICE_STATE(Custom:FOLLOWME{})'.format(_device), 'INVALID')
- if ast.QUEDEVSTATE == 'TRUE':
- await setQueueStates(queues, user, _device, 'NOT_INUSE')
- if ast.DNDDEVSTATE:
- await amiSetVar('DEVICE_STATE(Custom:DEVDND{})'.format(_device), 'NOT_INUSE')
- if ast.CFDEVSTATE:
- await amiSetVar('DEVICE_STATE(Custom:DEVCF{})'.format(_device), 'NOT_INUSE')
- await amiDBPut('DEVICE', '{}/user'.format(_device), 'none')
- async def setUserDeviceStates(user, device, queues, ast):
- if ast.FMDEVSTATE == 'TRUE':
- _followMe = await amiDBGet('AMPUSER', '{}/followme/ddial'.format(user))
- if _followMe is not None:
- await amiSetVar('DEVICE_STATE(Custom:FOLLOWME{})'.format(device), followMe2DevState(_followMe))
- if ast.QUEDEVSTATE == 'TRUE':
- await setQueueStates(queues, user, device, 'INUSE')
- if ast.DNDDEVSTATE:
- _dnd = await amiDBGet('DND', user)
- await amiSetVar('DEVICE_STATE(Custom:DEVDND{})'.format(device), 'INUSE' if _dnd == 'YES' else 'NOT_INUSE')
- if ast.CFDEVSTATE:
- _cf = await amiDBGet('CF', user)
- await amiSetVar('DEVICE_STATE(Custom:DEVCF{})'.format(device), 'INUSE' if _cf != '' else 'NOT_INUSE')
- async def refreshStatesCache():
- app.config['STATE_CACHE']['user'] = await amiExtensionStateList()
- app.config['STATE_CACHE']['presence'] = await amiPresenceStateList()
- return len(app.config['STATE_CACHE']['user'])
- async def userStateChangeCallback(user, state, prevState = None):
- reply = None
- if ((app.config['STATE_CALLBACK_URL'] not in NONEs) and
- ('HTTP_CLIENT' in app.config)):
- reply = await app.config['HTTP_CLIENT'].post(app.config['STATE_CALLBACK_URL'],
- json={'user': user,
- 'state': state,
- 'prev_state':prevState})
- else:
- app.logger.warning('{} changed state to: {}'.format(user, state))
- return reply
- def getUserStateCombined(user):
- _uCache = app.config['STATE_CACHE']['user']
- _pCache = app.config['STATE_CACHE']['presence']
- return combinedStates[_uCache.get(user, 'unavailable')][_pCache.get(user, 'not_set')]
- def getUsersStatesCombined():
- return {user:getUserStateCombined(user) for user in app.config['STATE_CACHE']['user']}
- @app.route('/atxfer/<userA>/<userB>')
- class AtXfer(Resource):
- @app.param('userA', 'User initiating the attended transfer', 'path')
- @app.param('userB', 'Transfer destination user', 'path')
- @app.response(HTTPStatus.OK, 'Json reply')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self, userA, userB):
- '''Attended call transfer
- '''
- device = await getUserDevice(userA)
- if device in NONEs:
- return noUserDevice(userA)
- channel = await amiDeviceChannel(device)
- if channel in NONEs:
- return noUserChannel(userA)
- reply = await manager.send_action({'Action':'Atxfer',
- 'Channel':channel,
- 'async':'false',
- 'Exten':userB})
- if isinstance(reply, Message):
- if reply.success:
- return successfullyTransfered(userA, userB)
- else:
- return errorReply(reply.message)
- @app.route('/users/states')
- class UsersStates(Resource):
- @app.response(HTTPStatus.OK, 'JSON reply with user:state map or error message')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self):
- '''Returns all users with their combined states.
- Possible states are: available, away, dnd, inuse, busy, unavailable, ringing
- '''
- usersCount = await refreshStatesCache()
- if usersCount == 0:
- return stateCacheEmpty()
- return successReply(getUsersStatesCombined())
- @app.route('/user/<user>/state')
- class UserState(Resource):
- @app.param('user', 'User to query for combined state', 'path')
- @app.response(HTTPStatus.OK, 'JSON data {"user":user,"state":state}')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self, user):
- '''Returns user's combined state.
- One of: available, away, dnd, inuse, busy, unavailable, ringing
- '''
- if user not in app.config['STATE_CACHE']['user']:
- return noUser(user)
- return successReply({'user':user,'state':getUserStateCombined(user)})
- @app.route('/user/<user>/presence')
- class PresenceState(Resource):
- @app.param('user', 'User to query for presence state', 'path')
- @app.response(HTTPStatus.OK, 'JSON data {"user":user,"state":state}')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self, user):
- '''Returns user's presence state.
- One of: not_set, unavailable, available, away, xa, chat, dnd
- '''
- if user not in app.config['STATE_CACHE']['user']:
- return noUser(user)
- return successReply({'user':user,'state':app.config['STATE_CACHE']['presence'].get(user, 'not_set')})
- @app.route('/user/<user>/presence/<state>')
- class SetPresenceState(Resource):
- @app.param('user', 'Target user to set the presence state', 'path')
- @app.param('state',
- 'The presence state for user, one of: not_set, unavailable, available, away, xa, chat or dnd',
- 'path')
- @app.response(HTTPStatus.OK, 'Json reply')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self, user, state):
- '''Sets user's presence state.
- Allowed states: not_set | unavailable | available | away | xa | chat | dnd
- '''
- if state not in presenceStates:
- return invalidState(state)
- if user not in app.config['STATE_CACHE']['user']:
- return noUser(user)
- result = await amiSetVar('PRESENCE_STATE(CustomPresence:{})'.format(user), state)
- if result is not None:
- return errorReply(result)
- return successfullySetState(user, state)
- @app.route('/users/devices')
- class UsersDevices(Resource):
- @app.response(HTTPStatus.OK, 'JSON reply with user:device map or error message')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self):
- '''Returns all users with their combined states.
- Possible states are: available, away, dnd, inuse, busy, unavailable, ringing
- '''
- data = {}
- for user in app.config['STATE_CACHE']['user']:
- device = await getUserDevice(user)
- if device in NONEs:
- device = None
- data[user]=device
- return successReply(data)
- @app.route('/device/<device>/<user>/on')
- @app.route('/user/<user>/<device>/on')
- class UserDeviceBind(Resource):
- @app.param('device', 'Device number to bind to', 'path')
- @app.param('user', 'User to bind to device', 'path')
- @app.response(HTTPStatus.OK, 'JSON reply with fields "success" and "result"')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self, device, user):
- '''Binds user to device.
- Both user and device numbers are checked for existance.
- Any device user was previously bound to, is unbound.
- Any user previously bound to device is unbound also.
- '''
- if user not in app.config['STATE_CACHE']['user']:
- return noUser(user)
- dial = await getDeviceDial(device) # Check if device exists in astdb
- if dial is None:
- return noDevice(device)
- currentUser = await getDeviceUser(device) # Check if any user is already bound to device
- if currentUser == user:
- return alreadyBound(user, device)
- ast = await getGlobalVars()
- queues = await amiQueues()
- if currentUser not in NONEs: # If any other user is bound to device, unbind him,
- await setUserDevice(currentUser, None)
- if ast.QUEDEVSTATE == 'TRUE': # set device states for previous user queues
- await setQueueStates(queues, currentUser, device, 'NOT_INUSE')
- await setUserHint(currentUser, None, ast) # set hints for previous user
- await setDeviceUser(device, user) # Bind user to device
- # If user is bound to some other devices, unbind him and set
- # device states for those devices
- await unbindOtherDevices(user, device, queues, ast)
- if not (await setUserHint(user, dial, ast)): # Set hints for user on new device
- return hintError(user, device)
- await setUserDeviceStates(user, device, queues, ast) # Set device states for users new device
- if not (await setUserDevice(user, device)): # Bind device to user
- return bindError(user, device)
- return successfullyBound(user, device)
- @app.route('/device/<device>/off')
- class DeviceUnBind(Resource):
- @app.param('device', 'Device number to unbind', 'path')
- @app.response(HTTPStatus.OK, 'JSON reply with fields "success" and "result"')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self, device):
- '''Unbinds any user from device.
- Device is checked for existance.
- '''
- dial = await getDeviceDial(device) # Check if device exists in astdb
- if dial is None:
- return noDevice(device)
- currentUser = await getDeviceUser(device) # Check if any user is bound to device
- if currentUser in NONEs:
- return noUserBound(device)
- else:
- ast = await getGlobalVars()
- queues = await amiQueues()
- await setUserDevice(currentUser, None) # Unbind device from current user
- if ast.QUEDEVSTATE == 'TRUE': # set device states for current user queues
- await setQueueStates(queues, currentUser, device, 'NOT_INUSE')
- await setUserHint(currentUser, None, ast) # set hints for current user
- await setDeviceUser(device, 'none') # Unbind user from device
- return successfullyUnbound(currentUser, device)
- @app.route('/cdr')
- class CDR(Resource):
- @app.param('end', 'End of datetime range. Defaults to now. Allowed formats are: timestamp, ISO 8601 and ddmmyyyyHHMMSS', 'query')
- @app.param('start', 'Start of datetime range. Defaults to end-24h. Allowed formats are: timestamp, ISO 8601 and ddmmyyyyHHMMSS', 'query')
- @app.response(HTTPStatus.OK, 'JSON reply')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self):
- '''Returns CDR data, groupped by logical call id.
- All request arguments are optional.
- '''
- start = parseDatetime(request.args.get('start'))
- end = parseDatetime(request.args.get('end'))
- cdr = await getCDR(start, end)
- return successReply(cdr)
- @app.route('/cel')
- class CEL(Resource):
- @app.param('end', 'End of datetime range. Defaults to now. Allowed formats are: timestamp, ISO 8601 and ddmmyyyyHHMMSS', 'query')
- @app.param('start', 'Start of datetime range. Defaults to end-24h. Allowed formats are: timestamp, ISO 8601 and ddmmyyyyHHMMSS', 'query')
- @app.response(HTTPStatus.OK, 'JSON reply')
- @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
- async def get(self):
- '''Returns CDR data, groupped by logical call id.
- All request arguments are optional.
- '''
- start = parseDatetime(request.args.get('start'))
- end = parseDatetime(request.args.get('end'))
- cel = await getCEL(start, end)
- return successReply(cel)
- manager.connect()
- app.run(loop=main_loop, host='0.0.0.0', port=app.config['PORT'])
|