diff --git a/IncomingCallRouting/ConfigurationManager.py b/IncomingCallRouting/ConfigurationManager.py new file mode 100644 index 0000000..4a3483b --- /dev/null +++ b/IncomingCallRouting/ConfigurationManager.py @@ -0,0 +1,23 @@ +import configparser + + +class ConfigurationManager: + __configuration = None + __instance = None + + def __init__(self): + if(self.__configuration == None): + self.__configuration = configparser.ConfigParser() + self.__configuration.read('config.ini') + + @staticmethod + def get_instance(): + if(ConfigurationManager.__instance == None): + ConfigurationManager.__instance = ConfigurationManager() + + return ConfigurationManager.__instance + + def get_app_settings(self, key): + if (key != None): + return self.__configuration.get('default', key) + return None diff --git a/IncomingCallRouting/Controllers/IncomingCallController.py b/IncomingCallRouting/Controllers/IncomingCallController.py new file mode 100644 index 0000000..f18b331 --- /dev/null +++ b/IncomingCallRouting/Controllers/IncomingCallController.py @@ -0,0 +1,82 @@ +import json +import ast +from typing import List +from aiohttp import web +from aiohttp.web_routedef import post +from Utils.Logger import Logger +from Utils.CallConfiguration import CallConfiguration +from azure.communication.callingserver.aio import CallingServerClient +from azure.eventgrid import EventGridEvent +from EventHandler.EventAuthHandler import EventAuthHandler +from EventHandler.EventDispatcher import EventDispatcher +from azure.core.messaging import CloudEvent +from Utils.IncomingCallHandler import IncomingCallHandler + + +class IncomingCallController: + + app = web.Application() + + _calling_server_client: CallingServerClient = None + _incoming_calls: List = None + _call_configuration: CallConfiguration = None + + def __init__(self, configuration): + self._call_configuration = CallConfiguration.get_call_configuration( + configuration) + self._calling_server_client = CallingServerClient.from_connection_string( + self._call_configuration.connection_string) + self._incoming_calls = [] + self.app.add_routes( + [web.post('/OnIncomingCall', self.on_incoming_call)]) + self.app.add_routes([web.post( + '/CallingServerAPICallBacks', self.calling_server_api_callbacks)]) + web.run_app(self.app, port=9007) + + + async def on_incoming_call(self, request): + try: + http_content = await request.content.read() + post_data = str(http_content.decode('UTF-8')) + if (post_data): + json_data = ast.literal_eval(json.dumps(post_data)) + cloud_event: EventGridEvent = EventGridEvent.from_dict( + ast.literal_eval(json_data)[0]) + + if(cloud_event.event_type == 'Microsoft.EventGrid.SubscriptionValidationEvent'): + event_data = cloud_event.data + code = event_data['validationCode'] + + if (code): + response_data = {"ValidationResponse": code} + if(response_data["ValidationResponse"] != None): + return web.Response(body=str(response_data), status=200) + elif (cloud_event.event_type == 'Microsoft.Communication.IncomingCall'): + if(post_data != None and cloud_event.data["to"]['rawId'] == self._call_configuration.bot_identity): + incoming_call_context = post_data.split( + "\"incomingCallContext\":\"")[1].split("\"}")[0] + self._incoming_calls.append(await IncomingCallHandler(self._calling_server_client, self._call_configuration).report(incoming_call_context)) + + return web.Response(status=200) + + except Exception as ex: + raise Exception("Failed to handle incoming call --> " + str(ex)) + + async def calling_server_api_callbacks(self, request): + try: + event_handler = EventAuthHandler() + param = request.rel_url.query + if (param.get('secret') and event_handler.authorize(param['secret'])): + if (request != None): + http_content = await request.content.read() + Logger.log_message( + Logger.INFORMATION, "CallingServerAPICallBacks -------> " + str(request)) + eventDispatcher: EventDispatcher = EventDispatcher.get_instance() + eventDispatcher.process_notification( + str(http_content.decode('UTF-8'))) + return web.Response(status=201) + else: + return web.Response(status=401) + except Exception as ex: + raise Exception( + "Failed to handle incoming callbacks --> " + str(ex)) diff --git a/IncomingCallRouting/Controllers/__init__.py b/IncomingCallRouting/Controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/IncomingCallRouting/EventHandler/EventAuthHandler.py b/IncomingCallRouting/EventHandler/EventAuthHandler.py new file mode 100644 index 0000000..191483b --- /dev/null +++ b/IncomingCallRouting/EventHandler/EventAuthHandler.py @@ -0,0 +1,15 @@ +from Utils.Logger import Logger + + +class EventAuthHandler: + + secret_value = 'h3llowW0rld' + + def authorize(self, query): + if query == None: + return False + return ((query != None) and (query == self.secret_value)) + + def get_secret_querystring(self): + secretKey = "secret" + return (secretKey + "=" + self.secret_value) diff --git a/IncomingCallRouting/EventHandler/EventDispatcher.py b/IncomingCallRouting/EventHandler/EventDispatcher.py new file mode 100644 index 0000000..b4ca008 --- /dev/null +++ b/IncomingCallRouting/EventHandler/EventDispatcher.py @@ -0,0 +1,104 @@ +import threading +import json +from Utils.Logger import Logger +from threading import Lock +from azure.core.messaging import CloudEvent +from azure.communication.callingserver import CallingServerEventType, \ + CallConnectionStateChangedEvent, ToneReceivedEvent, \ + PlayAudioResultEvent, TransferCallResultEvent + + +class EventDispatcher: + __instance = None + notification_callbacks: dict = None + subscription_lock = None + + def __init__(self): + self.notification_callbacks = dict() + self.subscription_lock = Lock() + + @staticmethod + def get_instance(): + if EventDispatcher.__instance is None: + EventDispatcher.__instance = EventDispatcher() + + return EventDispatcher.__instance + + def subscribe(self, event_type: str, event_key: str, notification_callback): + self.subscription_lock.acquire + event_id: str = self.build_event_key(event_type, event_key) + self.notification_callbacks[event_id] = notification_callback + self.subscription_lock.release + + def unsubscribe(self, event_type: str, event_key: str): + self.subscription_lock.acquire + event_id: str = self.build_event_key(event_type, event_key) + del self.notification_callbacks[event_id] + self.subscription_lock.release + + def build_event_key(self, event_type: str, event_key: str): + return event_type + "-" + event_key + + def process_notification(self, request: str): + call_event = self.extract_event(request) + if call_event is not None: + self.subscription_lock.acquire + notification_callback = self.notification_callbacks.get( + self.get_event_key(call_event)) + if (notification_callback != None): + + threading.Thread(target=notification_callback, + args=(call_event,)).start() + + def get_event_key(self, call_event_base): + if type(call_event_base) == CallConnectionStateChangedEvent: + call_leg_id = call_event_base.call_connection_id + key = self.build_event_key( + CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT, call_leg_id) + return key + elif type(call_event_base) == ToneReceivedEvent: + call_leg_id = call_event_base.call_connection_id + key = self.build_event_key( + CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id) + return key + elif type(call_event_base) == PlayAudioResultEvent: + operation_context = call_event_base.operation_context + key = self.build_event_key( + CallingServerEventType.PLAY_AUDIO_RESULT_EVENT, operation_context) + return key + elif type(call_event_base) == TransferCallResultEvent: + call_leg_id = call_event_base.operation_context + key = self.build_event_key( + CallingServerEventType.TRANSFER_CALL_RESULT_EVENT, call_leg_id) + return key + return None + + def extract_event(self, request: str): + try: + event = CloudEvent.from_dict(json.loads(request)[0]) + print(event) + if event.type == CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT: + call_connection_state_changed_event = CallConnectionStateChangedEvent.deserialize( + event.data) + return call_connection_state_changed_event + + if event.type == CallingServerEventType.PLAY_AUDIO_RESULT_EVENT: + play_audio_result_event = PlayAudioResultEvent.deserialize( + event.data) + return play_audio_result_event + + if event.type == CallingServerEventType.TRANSFER_CALL_RESULT_EVENT: + transfer_call_result_event = TransferCallResultEvent.deserialize( + event.data) + return transfer_call_result_event + + if event.type == CallingServerEventType.TONE_RECEIVED_EVENT: + tone_received_event = ToneReceivedEvent.deserialize( + event.data) + return tone_received_event + + except Exception as ex: + Logger.log_message( + Logger.ERROR, "Failed to parse request content Exception: " + str(ex)) + + return None diff --git a/IncomingCallRouting/EventHandler/__init__.py b/IncomingCallRouting/EventHandler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/IncomingCallRouting/Utils/CallConfiguration.py b/IncomingCallRouting/Utils/CallConfiguration.py new file mode 100644 index 0000000..35a3972 --- /dev/null +++ b/IncomingCallRouting/Utils/CallConfiguration.py @@ -0,0 +1,27 @@ +from EventHandler.EventAuthHandler import EventAuthHandler + + +class CallConfiguration: + callConfiguration = None + + def __init__(self, connection_string, app_base_url, audio_file_uri, participant, bot_identity): + self.connection_string: str = str(connection_string) + self.app_base_url: str = str(app_base_url) + self.audio_file_uri: str = str(audio_file_uri) + eventhandler = EventAuthHandler() + self.app_callback_url: str = app_base_url + \ + "/CallingServerAPICallBacks?" + eventhandler.get_secret_querystring() + self.target_participant: str = str(participant) + self.bot_identity: str = str(bot_identity) + + @classmethod + def get_call_configuration(cls, configuration): + if(cls.callConfiguration == None): + cls.callConfiguration = CallConfiguration( + configuration["connection_string"], + configuration["app_base_url"], + configuration["audio_file_uri"], + configuration["target_participant"], + configuration["bot_identity"]) + + return cls.callConfiguration diff --git a/IncomingCallRouting/Utils/CommunicationIdentifierKind.py b/IncomingCallRouting/Utils/CommunicationIdentifierKind.py new file mode 100644 index 0000000..8fc375e --- /dev/null +++ b/IncomingCallRouting/Utils/CommunicationIdentifierKind.py @@ -0,0 +1,8 @@ +import enum + + +class CommunicationIdentifierKind(enum.Enum): + + USER_IDENTITY = "USER_IDENTITY" + PHONE_IDENTITY = "PHONE_IDENTITY" + UNKNOWN_IDENTITY = "UNKNOWN_IDENTITY" diff --git a/IncomingCallRouting/Utils/Constants.py b/IncomingCallRouting/Utils/Constants.py new file mode 100644 index 0000000..3efb2c2 --- /dev/null +++ b/IncomingCallRouting/Utils/Constants.py @@ -0,0 +1,7 @@ +import enum + + +class Constants(enum.Enum): + + userIdentityRegex = "8:acs:[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}_[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}" + phoneIdentityRegex = "^\+\d{10,14}$" diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py new file mode 100644 index 0000000..6907963 --- /dev/null +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -0,0 +1,272 @@ +import re +import traceback +import uuid +import asyncio +from Utils.Logger import Logger +from Utils.Constants import Constants +from Utils.CommunicationIdentifierKind import CommunicationIdentifierKind +from Utils.CallConfiguration import CallConfiguration +from EventHandler.EventDispatcher import EventDispatcher +from azure.communication.callingserver.aio import CallingServerClient +from azure.communication.callingserver import CallConnectionStateChangedEvent, ToneReceivedEvent, ToneInfo, PlayAudioResultEvent, CallMediaType, CallingEventSubscriptionType, CallConnectionState, CallingOperationStatus, ToneValue, CallingServerEventType, TransferCallResultEvent, CommunicationUserIdentifier, PhoneNumberIdentifier +# from azure.communication.identity import + +PLAY_AUDIO_AWAIT_TIMER = 10 + + +class IncomingCallHandler: + _calling_server_client = None + _call_configuration = None + _call_connection = None + _target_participant = None + + _call_established_task: asyncio.Future = None + _play_audio_completed_task: asyncio.Future = None + _tone_received_completed_task: asyncio.Future = None + _transfer_to_participant_complete_task: asyncio.Future = None + _max_retry_attempt_count = 3 + + def __init__(self, calling_server_client: CallingServerClient, call_configuration: CallConfiguration): + self._call_configuration = call_configuration + self._calling_server_client = calling_server_client + self._target_participant = call_configuration.target_participant + + async def report(self, incomming_call_context: str): + try: + # answer call + response = await self._calling_server_client.answer_call( + incomming_call_context, + requested_media_types={CallMediaType.AUDIO}, + requested_call_events={ + CallingEventSubscriptionType.PARTICIPANTS_UPDATED, CallingEventSubscriptionType.TONE_RECEIVED}, + callback_uri=self._call_configuration.app_callback_url + ) + + Logger.log_message(Logger.INFORMATION, + "AnswerCall Response ----->" + str(response)) + + self._call_connection = self._calling_server_client.get_call_connection(response.call_connection_id) + self._register_to_call_state_change_event( + self._call_connection.call_connection_id) + + # wait for the call to get connected + await self._call_established_task + + self._register_to_dtmf_result_event( + self._call_connection.call_connection_id) + + await self._play_audio_async() + play_audio_completed = await self._play_audio_completed_task + + if(play_audio_completed == False): + await self._hang_up_async() + else: + tone_received_completed_task = await self._tone_received_completed_task + transfer_to_participant_completed = False + if(tone_received_completed_task == True): + participant: str = self._target_participant + Logger.log_message( + Logger.INFORMATION, "Transfering call to participant -----> " + participant) + transfer_to_participant_completed = await self._transfer_to_participant(participant) + if(transfer_to_participant_completed == False): + await self._retry_transfer_to_participant_async(participant) + if(transfer_to_participant_completed == False): + await self._hang_up_async() + except Exception as ex: + Logger.log_message(Logger.ERROR, + "Call ended unexpectedly, reason: " + str(ex)) + raise Exception( + "Failed to report incoming call --> " + str(ex)) + + async def _retry_transfer_to_participant_async(self, participant): + retry_attempt_count = 1 + while(retry_attempt_count <= self._max_retry_attempt_count): + Logger.log_message(Logger.INFORMATION, + "Retrying Transfer participant attempt " + str(retry_attempt_count) + " is in progress") + transfer_to_participant_result = await self._transfer_to_participant(participant) + if(transfer_to_participant_result): + return + else: + Logger.log_message(Logger.INFORMATION, + "Retrying Transfer participant attempt " + str(retry_attempt_count) + " has failed") + retry_attempt_count += 1 + + async def _play_audio_async(self): + try: + operation_context = str(uuid.uuid4()) + play_audio_response = await self._call_connection.play_audio( + audio_url=self._call_configuration.audio_file_uri, + is_looped=True, + operation_context=operation_context + ) + Logger.log_message(Logger.INFORMATION, "PlayAudioAsync response --> " + str(play_audio_response) + ", Id: " + play_audio_response.operation_id + + ", Status: " + play_audio_response.status + ", OperationContext: " + str(play_audio_response.operation_context) + ", ResultInfo: " + str(play_audio_response.result_details)) + + if (play_audio_response.status == CallingOperationStatus.RUNNING): + Logger.log_message(Logger.INFORMATION, + "Play Audio state: " + play_audio_response.status) + # listen to play audio events + self._register_to_play_audio_result_event( + play_audio_response.operation_context) + + tasks = [] + tasks.append(self._play_audio_completed_task) + tasks.append(asyncio.create_task( + asyncio.sleep(PLAY_AUDIO_AWAIT_TIMER))) + + await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + if (not self._play_audio_completed_task.done()): + try: + # cancel playing audio + await self._cancel_all_media_operations() + self._play_audio_completed_task.set_result(False) + except Exception as ex: + pass + except Exception as ex: + Logger.log_message( + Logger.ERROR, "Failure occured while playing audio on the call. Exception: " + str(ex)) + + async def _hang_up_async(self): + Logger.log_message(Logger.INFORMATION, + "Performing Hangup operation") + await self._call_connection.hang_up() + + async def _cancel_all_media_operations(self): + Logger.log_message(Logger.INFORMATION, + "Cancellation request, CancelMediaProcessing will be performed") + await self._call_connection.cancel_all_media_operations() + + def _register_to_call_state_change_event(self, call_leg_id): + self._call_terminated_task = asyncio.Future() + self._call_established_task = asyncio.Future() + + # set the callback method + def call_state_change_notificaiton(call_event): + try: + call_state_changes: CallConnectionStateChangedEvent = call_event + Logger.log_message( + Logger.INFORMATION, "Call State changed to -- > " + call_state_changes.call_connection_state) + + if (call_state_changes.call_connection_state == CallConnectionState.CONNECTED): + Logger.log_message(Logger.INFORMATION, + "Call State successfully connected") + self._call_established_task.set_result(True) + + elif (call_state_changes.call_connection_state == CallConnectionState.DISCONNECTED): + EventDispatcher.get_instance().unsubscribe( + CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT, call_leg_id) + self._call_terminated_task.set_result(True) + + except asyncio.InvalidStateError: + pass + + # Subscribe to the event + EventDispatcher.get_instance().subscribe(CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT, + call_leg_id, call_state_change_notificaiton) + + def _register_to_play_audio_result_event(self, operation_context): + self._play_audio_completed_task = asyncio.Future() + + def play_prompt_response_notification(call_event): + play_audio_result_event: PlayAudioResultEvent = call_event + Logger.log_message( + Logger.INFORMATION, "Play audio status -- > " + str(play_audio_result_event.status)) + + if (play_audio_result_event.status == CallingOperationStatus.COMPLETED): + EventDispatcher.get_instance().unsubscribe( + CallingServerEventType.PLAY_AUDIO_RESULT_EVENT, operation_context) + try: + self._play_audio_completed_task.set_result(True) + except: + pass + elif (play_audio_result_event.status == CallingOperationStatus.FAILED): + try: + self._play_audio_completed_task.set_result(False) + except: + pass + + # Subscribe to event + EventDispatcher.get_instance().subscribe(CallingServerEventType.PLAY_AUDIO_RESULT_EVENT, + operation_context, play_prompt_response_notification) + + def _register_to_dtmf_result_event(self, call_leg_id): + self._tone_received_completed_task = asyncio.Future() + loop = asyncio.get_event_loop() + + def dtmf_received_event(call_event): + tone_received_event: ToneReceivedEvent = call_event + tone_info: ToneInfo = tone_received_event.tone_info + + Logger.log_message(Logger.INFORMATION, + "Tone received -- > : " + str(tone_info.tone)) + + if (tone_info.tone == ToneValue.TONE1): + try: + self._tone_received_completed_task.set_result(True) + except: + pass + else: + try: + self._tone_received_completed_task.set_result(False) + except: + pass + + EventDispatcher.get_instance().unsubscribe( + CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id) + + + # cancel playing audio + future = asyncio.run_coroutine_threadsafe(self._cancel_all_media_operations(), loop) + future.result() + + try: + self._play_audio_completed_task.set_result(True) + except: + pass + + # Subscribe to event + EventDispatcher.get_instance().subscribe( + CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id, dtmf_received_event) + + async def _transfer_to_participant(self, target_participant: str): + self._transfer_to_participant_complete_task = asyncio.Future() + identifier_kind = self._get_identifier_kind(target_participant) + + if (identifier_kind == CommunicationIdentifierKind.UNKNOWN_IDENTITY): + Logger.log_message(Logger.INFORMATION, "Unknown identity provided. Enter valid phone number or communication user id") + try: + self._transfer_to_participant_complete_task.set_result(True) + except: + pass + else: + operation_context = str(uuid.uuid4()) + self._register_to_transfer_participants_result_event(operation_context) + if (identifier_kind == CommunicationIdentifierKind.USER_IDENTITY): + identifier = CommunicationUserIdentifier(target_participant) + response = await self._call_connection.transfer_to_participant(identifier, operation_context = operation_context) + Logger.log_message(Logger.INFORMATION, "TransferParticipantAsync response --> " + str(response) + ", status: " + response.status + + ", OperationContext: " + response.operation_context + ", OperationId: " + response.operation_id + ", ResultDetails: " + str(response.result_details)) + elif (identifier_kind == CommunicationIdentifierKind.PHONE_IDENTITY): + identifier = PhoneNumberIdentifier(target_participant) + response = await self._call_connection.transfer_to_participant(identifier, operation_context = operation_context) + Logger.log_message(Logger.INFORMATION, "TransferParticipantAsync response --> " + str(response)) + + transfer_to_participant_completed = await self._transfer_to_participant_complete_task + return transfer_to_participant_completed + + + def _register_to_transfer_participants_result_event(self, operation_context: str): + def transfer_to_participant_received_event(call_event): + transfer_call_result_event: TransferCallResultEvent = call_event + if(transfer_call_result_event != None): + Logger.log_message(Logger.INFORMATION, "Transfer participant callconnection ID - " + transfer_call_result_event.operation_context) + EventDispatcher.get_instance().unsubscribe(CallingServerEventType.TRANSFER_CALL_RESULT_EVENT, transfer_call_result_event.operation_context) + self._transfer_to_participant_complete_task.set_result(True) + else: + self._transfer_to_participant_complete_task.set_result(False) + + EventDispatcher.get_instance().subscribe(CallingServerEventType.TRANSFER_CALL_RESULT_EVENT, operation_context, transfer_to_participant_received_event) + + + def _get_identifier_kind(self, participant_number: str): + return CommunicationIdentifierKind.USER_IDENTITY if re.search(Constants.userIdentityRegex.value, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.PHONE_IDENTITY if re.search(Constants.phoneIdentityRegex.value, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.UNKNOWN_IDENTITY \ No newline at end of file diff --git a/IncomingCallRouting/Utils/Logger.py b/IncomingCallRouting/Utils/Logger.py new file mode 100644 index 0000000..8d46327 --- /dev/null +++ b/IncomingCallRouting/Utils/Logger.py @@ -0,0 +1,16 @@ +import enum +import logging + + +class Logger(enum.Enum): + + INFORMATION = 1 + ERROR = 2 + + @staticmethod + def log_message(message_type, message): + log_message = message_type.name + " : " + message + if message_type == Logger.ERROR: + logging.error(log_message) + else: + logging.info(log_message) diff --git a/IncomingCallRouting/Utils/__init__.py b/IncomingCallRouting/Utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/IncomingCallRouting/__init__.py b/IncomingCallRouting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/IncomingCallRouting/config.ini b/IncomingCallRouting/config.ini new file mode 100644 index 0000000..2a8da8b --- /dev/null +++ b/IncomingCallRouting/config.ini @@ -0,0 +1,15 @@ +# app settings +[default] +# Configurations related to Communication Service resource +Connectionstring=%Connectionstring% + +BaseUrl=%BaseUrl% + +# public url of wav audio +AudioFileUri=%AudioFileUri% + +# participant (PhoneNumber/MRI) +TargetParticipant=%TargetParticipant% + +# identity for the bot +BotIdentity=%BotIdentity% \ No newline at end of file diff --git a/IncomingCallRouting/dependencies/Azure.Communication.CallingServer.dll b/IncomingCallRouting/dependencies/Azure.Communication.CallingServer.dll new file mode 100644 index 0000000..bf29c3f Binary files /dev/null and b/IncomingCallRouting/dependencies/Azure.Communication.CallingServer.dll differ diff --git a/IncomingCallRouting/program.py b/IncomingCallRouting/program.py new file mode 100644 index 0000000..dc72da7 --- /dev/null +++ b/IncomingCallRouting/program.py @@ -0,0 +1,18 @@ +from Controllers.IncomingCallController import IncomingCallController +from Utils.CallConfiguration import CallConfiguration +from ConfigurationManager import ConfigurationManager +import asyncio + +if __name__ == '__main__': + config_manager = ConfigurationManager.get_instance() + config = { + "connection_string": config_manager.get_app_settings("Connectionstring"), + "app_base_url": config_manager.get_app_settings("BaseUrl"), + "audio_file_uri": config_manager.get_app_settings("AudioFileUri"), + "target_participant": config_manager.get_app_settings("TargetParticipant"), + "bot_identity": config_manager.get_app_settings("BotIdentity") + } + + loop = asyncio.get_event_loop() + loop.run_until_complete(IncomingCallController(config)) + diff --git a/IncomingCallRouting/readme.md b/IncomingCallRouting/readme.md new file mode 100644 index 0000000..0bead66 --- /dev/null +++ b/IncomingCallRouting/readme.md @@ -0,0 +1,41 @@ +--- +page_type: sample +languages: + - python +products: + - azure + - azure-communication-services +--- + +# Incoming Call Routing Sample + +This sample application shows how the Azure Communication Services Server, Calling package can be used to build IVR related solutions. This sample answer an incoming call from a phone number or a communication identifier and plays an audio message. If the caller presses 1 (tone1), the application will transfer the call. If the caller presses any other key then the application will ends the call after playing the audio message for a few times. The application is a console based application build using Python 3.9. + +## Getting started + +### Prerequisites + +- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/) +- [Python](https://www.python.org/downloads/) 3.9 and above +- Create an Azure Communication Services resource. For details, see [Create an Azure Communication Resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). You'll need to record your resource **connection string** for this sample. +- Download and install [Ngrok](https://www.ngrok.com/download). As the sample is run locally, Ngrok will enable the receiving of all the events. +- Download and install [VSCode](https://code.visualstudio.com/) + +> Note: the samples make use of the Microsoft Cognitive Services Speech SDK. By downloading the Microsoft Cognitive Services Speech SDK, you acknowledge its license, see [Speech SDK license agreement](https://aka.ms/csspeech/license201809). + +### Configuring application + +- Open the config.ini file to configure the following settings + + - Connection String: Azure Communication Service resource's connection string. + - Base Url: base url of the endpoint + - Audio File Uri: uri of the audio file + - Target Participant: phone number/MRI of the participant + - Bot Identity: identity of the bot + +### Run the Application + +- Add azure communication callingserver's wheel file path in requirement.txt +- Navigate to the directory containing the requirements.txt file and use the following commands for installing all the dependencies and for running the application respectively: + - pip install -r requirements.txt + - python program.py diff --git a/IncomingCallRouting/requirements.txt b/IncomingCallRouting/requirements.txt new file mode 100644 index 0000000..9868876 --- /dev/null +++ b/IncomingCallRouting/requirements.txt @@ -0,0 +1,34 @@ +aiohttp==3.7.4.post0 +async-timeout==3.0.1 +attrs==21.2.0 +azure-cognitiveservices-speech==1.18.0 +azure-common==1.1.27 +# format : @ file:///D:/sdk/dist/azure_communication_callingserver-1.0.0b1-py2.py3-none-any.whl +azure-communication-callingserver @ file:///C:/sources/azure-sdk-for-python/sdk/communication/azure-communication-callingserver/dist/azure_communication_callingserver-1.0.0b1-py2.py3-none-any.whl +azure-communication-chat==1.0.0 +azure-communication-identity==1.0.1 + +azure-core==1.19.1 +azure-nspkg==3.0.2 +azure-storage==0.36.0 +azure-eventgrid +certifi==2021.5.30 +cffi==1.14.6 +chardet==4.0.0 +charset-normalizer==2.0.4 +cryptography==3.4.8 +idna==3.2 +isodate==0.6.0 +msrest==0.6.21 +multidict==5.1.0 +nest-asyncio==1.5.1 +oauthlib==3.1.1 +psutil==5.8.0 +pycparser==2.20 +python-dateutil==2.8.2 +requests==2.26.0 +requests-oauthlib==1.3.0 +six==1.16.0 +typing-extensions==3.10.0.0 +urllib3==1.26.6 +yarl==1.6.3 \ No newline at end of file