From ab201146b332d7d0baf5b29103b66719f3aa2869 Mon Sep 17 00:00:00 2001 From: Torres Yang Date: Thu, 23 Dec 2021 22:14:07 -0800 Subject: [PATCH 01/15] Added Event Handler classes --- .../EventHandler/EventAuthHandler.py | 15 +++ .../EventHandler/EventDispatcher.py | 100 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 IncomingCallRouting/EventHandler/EventAuthHandler.py create mode 100644 IncomingCallRouting/EventHandler/EventDispatcher.py diff --git a/IncomingCallRouting/EventHandler/EventAuthHandler.py b/IncomingCallRouting/EventHandler/EventAuthHandler.py new file mode 100644 index 0000000..0b629b3 --- /dev/null +++ b/IncomingCallRouting/EventHandler/EventAuthHandler.py @@ -0,0 +1,15 @@ +from 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..c0d80e7 --- /dev/null +++ b/IncomingCallRouting/EventHandler/EventDispatcher.py @@ -0,0 +1,100 @@ +from Logger import Logger +from threading import Lock +import threading +from azure.core.messaging import EventGridEvent +from azure.communication.callingserver import CallingServerEventType, \ + CallConnectionStateChangedEvent, ToneReceivedEvent, \ + PlayAudioResultEvent, ParticipantsUpdatedEvent + + +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, cloudEvent: EventGridEvent): + call_event = self.extract_event(cloudEvent) + 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) == ParticipantsUpdatedEvent: + call_leg_id = call_event_base.call_connection_id + key = self.build_event_key( + CallingServerEventType.PARTICIPANTS_UPDATED_EVENT, call_leg_id) + return key + return None + + def extract_event(self, cloudEvent: EventGridEvent): + try: + if cloudEvent.event_type == CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT: + call_connection_state_changed_event = CallConnectionStateChangedEvent.deserialize( + cloudEvent.data) + return call_connection_state_changed_event + + if cloudEvent.event_type == CallingServerEventType.PLAY_AUDIO_RESULT_EVENT: + play_audio_result_event = PlayAudioResultEvent.deserialize( + cloudEvent.data) + return play_audio_result_event + + if cloudEvent.event_type == CallingServerEventType.PARTICIPANTS_UPDATED_EVENT: + add_participant_result_event = ParticipantsUpdatedEvent.deserialize( + cloudEvent.data) + return add_participant_result_event + + if cloudEvent.event_type == CallingServerEventType.TONE_RECEIVED_EVENT: + tone_received_event = ToneReceivedEvent.deserialize( + cloudEvent.data) + return tone_received_event + + except Exception as ex: + Logger.log_message( + Logger.ERROR, "Failed to parse request content Exception: " + str(ex)) + + return None From 6ad15d64170d89d6660f2880613760aa06fea2ab Mon Sep 17 00:00:00 2001 From: Torres Yang Date: Thu, 23 Dec 2021 22:21:29 -0800 Subject: [PATCH 02/15] Added utils classes and dependencies --- .../Utils/CallConfiguration.py | 25 ++++++++++++++++++ .../Utils/CommunicationIdentifierKind.py | 8 ++++++ IncomingCallRouting/Utils/Constants.py | 7 +++++ IncomingCallRouting/Utils/Logger.py | 16 +++++++++++ .../Azure.Communication.CallingServer.dll | Bin 0 -> 329216 bytes 5 files changed, 56 insertions(+) create mode 100644 IncomingCallRouting/Utils/CallConfiguration.py create mode 100644 IncomingCallRouting/Utils/CommunicationIdentifierKind.py create mode 100644 IncomingCallRouting/Utils/Constants.py create mode 100644 IncomingCallRouting/Utils/Logger.py create mode 100644 IncomingCallRouting/dependencies/Azure.Communication.CallingServer.dll diff --git a/IncomingCallRouting/Utils/CallConfiguration.py b/IncomingCallRouting/Utils/CallConfiguration.py new file mode 100644 index 0000000..54215f7 --- /dev/null +++ b/IncomingCallRouting/Utils/CallConfiguration.py @@ -0,0 +1,25 @@ +from EventHandler.EventAuthHandler import EventAuthHandler + + +class CallConfiguration: + callConfiguration = None + + def __init__(self, connection_string, app_base_url, audio_file_name, participant): + self.connection_string: str = str(connection_string) + self.app_base_url: str = str(app_base_url) + self.audio_file_name: str = str(audio_file_name) + eventhandler = EventAuthHandler() + self.app_callback_url: str = app_base_url + \ + "/CallingServerAPICallBacks?" + eventhandler.get_secret_querystring() + self.audio_file_url: str = app_base_url + "/audio/" + audio_file_name + self.targetParticipant: str = str(participant) + + def get_call_configuration(self, configuration): + if(self.callConfiguration != None): + self.callConfiguration = CallConfiguration( + self, configuration['connection_string'], + configuration['app_base_url'], + configuration['audio_file_name'], + configuration['participant']) + + return self.callConfiguration diff --git a/IncomingCallRouting/Utils/CommunicationIdentifierKind.py b/IncomingCallRouting/Utils/CommunicationIdentifierKind.py new file mode 100644 index 0000000..571b625 --- /dev/null +++ b/IncomingCallRouting/Utils/CommunicationIdentifierKind.py @@ -0,0 +1,8 @@ +import enum + + +class CommunicationIdentifierKind(enum.Enum): + + USER_IDENTITY = 1 + PHONE_IDENTITY = 2 + UNKNOWN_IDENTITY = 3 diff --git a/IncomingCallRouting/Utils/Constants.py b/IncomingCallRouting/Utils/Constants.py new file mode 100644 index 0000000..a5b82ba --- /dev/null +++ b/IncomingCallRouting/Utils/Constants.py @@ -0,0 +1,7 @@ +import enum + + +class Constant(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/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/dependencies/Azure.Communication.CallingServer.dll b/IncomingCallRouting/dependencies/Azure.Communication.CallingServer.dll new file mode 100644 index 0000000000000000000000000000000000000000..bf29c3f84e0794b7bc3f5844d9c6f21254a711d5 GIT binary patch literal 329216 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 literal 0 HcmV?d00001 From 8adc881d6b57b8d47c24f93eaefa936c813a1991 Mon Sep 17 00:00:00 2001 From: James Deng Date: Wed, 5 Jan 2022 10:04:41 -0800 Subject: [PATCH 03/15] add incomming call handler --- .../Utils/IncomingCallHandler.py | 256 ++++++++++++++++++ IncomingCallRouting/requirements.txt | 32 +++ 2 files changed, 288 insertions(+) create mode 100644 IncomingCallRouting/Utils/IncomingCallHandler.py create mode 100644 IncomingCallRouting/requirements.txt diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py new file mode 100644 index 0000000..57c355a --- /dev/null +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -0,0 +1,256 @@ +import re +import traceback +import uuid +import asyncio +import Constants +import CommunicationIdentifierKind +from Logger import Logger +from CommunicationIdentifierKind import CommunicationIdentifierKind +from EventHandler.EventDispatcher import EventDispatcher +from CallConfiguration import CallConfiguration +from azure.communication.callingserver.aio import CallingServerClient, CancellationTokenSource, CallConnection, CallConnectionStateChangedEvent, ToneReceivedEvent, ToneInfo, PlayAudioResultEvent, AddParticipantResultEvent, CallMediaType, CallingEventSubscriptionType, CreateCallOptions, CallConnectionState, CallingOperationStatus, ToneValue, PlayAudioOptions, CallingServerEventType, PlayAudioResult, AddParticipantResult +from azure.communication.callingserver import * +from azure.communication.identity._shared.models import * + +PLAY_AUDIO_AWAIT_TIMER = 10 + +class IncomingCallHandler: + _calling_server_client = None + _call_configuration = None + _call_connection = None + _report_cancellation_token_source = None + _report_cancellation_token = None + _target_participant = None + + _call_estabished_task: asyncio.Future = None + _play_audio_completed_task: asyncio.Future = None + _call_terminatied_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.targetParticipantpython + + async def report(self, incomming_call_context: str): + self._report_cancellation_token_source = CancellationTokenSource() + self._report_cancellation_token = self._report_cancellation_token_source.Token + + try: + # wait for 10 sec before answering the call. + await asyncio.sleep(10 * 1000) + + # answer call + response = await self._calling_server_client.answer_call( + self._incoming_call_context, + requested_media_types = {CallMediaType.AUDIO}, + requested_call_events = {CallingEventSubscriptionType.PARTICIPANTS_UPDATED, CallingEventSubscriptionType.TONE_RECEIVED}, + callback_uri = self._call_configuration.appCallbackUrl + ) + + Logger.log_message(Logger.MessageType.INFORMATION, "AnswerCall Response ----->", response.ToString()) + + self._call_connection = response.value + register_to_call_state_change_event(self._call_connection.call_connection_id) + + # wait for the call to get connected + await self._call_estabished_task() + + register_to_dtmf_result_event(self._call_connection.callConnectionId) + + await self.play_audio_async() + play_audio_completed = await self._play_audio_completed_task + + if(play_audio_completed == False): + await hang_up_async() + else: + tone_received_completed_task = await self._tone_received_completed_task() + if(tone_received_completed_task == True): + participant: str = self._target_participant + Logger.log_message(Logger.MessageType.INFORMATION, "Transfering call to participant ----->", participant) + transfer_to_participant_completed = await transfer_to_participant(participant) + if(transfer_to_participant_completed == False): + await retry_transfer_to_participant_async(participant) + await hang_up_async() + await self._call_termination_task() + except Exception as ex: + Logger.log_message(Logger.MessageType.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.MessageType.INFORMATION, "Retrying Transfer participant attempt ", retry_attempt_count, " is in progress") + transfer_to_participant_result = await transfer_to_participant(participant) + if(transfer_to_participant_result): + return + else: + Logger.log_message(Logger.MessageType.INFORMATION, "Retrying Transfer participant attempt ", 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_url, + is_looped = True, + operation_context = operation_context + ) + Logger.log_message(Logger.MesMessageTypes.INFORMATION, "PlayAudioAsync response --> ", response.GetRawResponse(), ", Id: ", response.Value.OperationId, ", Status: ", response.Value.Status, ", OperationContext: ", response.Value.OperationContext, ", ResultInfo: ", response.Value.ResultDetails) + + if (play_audio_response.Value.Status == CallingOperationStatus.Running): + Logger.log_message(Logger.MessageType.INFORMATION, "Play Audio state: ", response.Value.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: + self.play_audio_completed_task.set_result(True) + except Exception as ex: + pass + try: + # After playing audio for 10 sec, make toneReceivedCompleteTask true. + self.tone_received_complete_task.set_result(True) + except Exception as ex: + pass + except TaskCanceledException as tce: + Logger.log_message(Logger.MessageType.ERROR, "Play audio operation cancelled") + except Exception as ex: + Logger.log_message(Logger.MessageType.ERROR, "Failure occured while playing audio on the call. Exception: ", str(ex)) + + async def hang_up_async(self): + if(self._report_cancellation_token.isCancellationRequested): + Logger.log_message(Logger.MessageType.INFORMATION, "Cancellation request, Hangup will not be performed") + return + Logger.log_message(Logger.MessageType.INFORMATION, "Performing Hangup operation") + hang_up_response = await self._call_connection.hang_up_async(self._report_cancellation_token) + Logger.log_message(Logger.MessageType.INFORMATION, "hang_up_async response -----> ", hang_up_response) + + + async def cancel_all_media_operations(self): + if(self._report_cancellation_token.isCancellationRequested): + Logger.log_message(Logger.MessageType.INFORMATION, "Cancellation request, CancelMediaProcessing will not be performed") + return + Logger.log_message(Logger.MessageType.INFORMATION, "Cancellation request, CancelMediaProcessing will not be performed") + + operation_context = str(uuid.uuid4()) + response = await self._call_connection.CancelAllMediaOperationsAsync(operation_context, self._report_cancellation_token) + + Logger.log_message(Logger.MessageType.INFORMATION, "PlayAudioAsync response --> ", response.ContentStream, ", Id: ", response.Content, ", Status: ", response.Status) + + + + + + + + + + + + + + def register_to_call_state_change_event(self, call_leg_id): + self.call_terminated_task = asyncio.Future() + self.call_connected_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_connected_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 cancel_media_processing(self): + Logger.log_message( + Logger.INFORMATION, "Performing cancel media processing operation to stop playing audio") + + self.call_connection.cancel_all_media_operations() + + 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_complete_task = asyncio.Future() + + 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_complete_task.set_result(True) + except: + pass + else: + try: + self.tone_received_complete_task.set_result(False) + except: + pass + + EventDispatcher.get_instance().unsubscribe( + CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id) + # cancel playing audio + self.cancel_media_processing() + + # Subscribe to event + EventDispatcher.get_instance().subscribe( + CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id, dtmf_received_event) + + + def _get_identifier_kind(participant_number: str): + return CommunicationIdentifierKind.USER_IDENTITY if re.search(Constants.userIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.PHONE_IDENTITY if re.search(Constants.phoneIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.UNKNOWN_IDENTITY diff --git a/IncomingCallRouting/requirements.txt b/IncomingCallRouting/requirements.txt new file mode 100644 index 0000000..c55b074 --- /dev/null +++ b/IncomingCallRouting/requirements.txt @@ -0,0 +1,32 @@ +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 +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 From e97c6c1c4b874e597ade64a01a6b1c339e1edf32 Mon Sep 17 00:00:00 2001 From: Torres Yang Date: Wed, 5 Jan 2022 17:04:10 -0800 Subject: [PATCH 04/15] Finished controller and webapp classes --- .vscode/settings.json | 3 + IncomingCallRouting/ConfigurationManager.py | 23 ++++++ .../Controllers/IncomingCallController.py | 81 +++++++++++++++++++ IncomingCallRouting/Controllers/__init__.py | 0 IncomingCallRouting/EventHandler/__init__.py | 0 .../Utils/CallConfiguration.py | 13 ++- .../Utils/IncomingCallHandler.py | 2 +- IncomingCallRouting/Utils/__init__.py | 0 IncomingCallRouting/__init__.py | 0 IncomingCallRouting/config.ini | 11 +++ IncomingCallRouting/program.py | 17 ++++ 11 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 IncomingCallRouting/ConfigurationManager.py create mode 100644 IncomingCallRouting/Controllers/IncomingCallController.py create mode 100644 IncomingCallRouting/Controllers/__init__.py create mode 100644 IncomingCallRouting/EventHandler/__init__.py create mode 100644 IncomingCallRouting/Utils/__init__.py create mode 100644 IncomingCallRouting/__init__.py create mode 100644 IncomingCallRouting/config.ini create mode 100644 IncomingCallRouting/program.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..157d539 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.extraPaths": ["./IncomingCallRouting/Utils"] +} 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..79f9b6a --- /dev/null +++ b/IncomingCallRouting/Controllers/IncomingCallController.py @@ -0,0 +1,81 @@ +from typing import List +from aiohttp import web +from aiohttp.web_routedef import post +from Logger import Logger +import json +import ast +from CallConfiguration import CallConfiguration +from azure.communication.callingserver import CallingServerClient +from EventHandler.EventAuthHandler import EventAuthHandler +from EventHandler.EventDispatcher import EventDispatcher +from azure.messaging.eventgrid import SystemEvents, EventGridEvent +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.app.add_routes( + [web.post('/OnIncomingCall', self.on_incoming_call)]) + self.app.add_routes([web.get( + '/CallingServerAPICallBacks', self.calling_server_api_callbacks)]) + web.run_app(self.app, port=9007) + + self._calling_server_client = CallingServerClient( + configuration['ResourceConnectionString']) + self._incoming_calls = [] + self._call_configuration = CallConfiguration.get_call_configuration( + configuration) + + 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.EventType == 'Microsoft.Communication.IncomingCall'): + event_data = str(request) + if(event_data != None): + incoming_call_context = event_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, secret: str): + try: + eventHandler = EventAuthHandler() + if EventAuthHandler.authorize(secret): + if request != None: + http_content = await request.content.read() + Logger.log_message( + Logger.MessageType.INFORMATION, "CallingServerAPICallBacks-------> {request.ToString()}") + 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/__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 index 54215f7..aa92827 100644 --- a/IncomingCallRouting/Utils/CallConfiguration.py +++ b/IncomingCallRouting/Utils/CallConfiguration.py @@ -4,22 +4,21 @@ class CallConfiguration: callConfiguration = None - def __init__(self, connection_string, app_base_url, audio_file_name, participant): + def __init__(self, connection_string, app_base_url, audio_file_uri, participant): self.connection_string: str = str(connection_string) self.app_base_url: str = str(app_base_url) - self.audio_file_name: str = str(audio_file_name) + 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.audio_file_url: str = app_base_url + "/audio/" + audio_file_name self.targetParticipant: str = str(participant) def get_call_configuration(self, configuration): if(self.callConfiguration != None): self.callConfiguration = CallConfiguration( - self, configuration['connection_string'], - configuration['app_base_url'], - configuration['audio_file_name'], - configuration['participant']) + configuration["connection_string"], + configuration["app_base_url"], + configuration["audio_file_uri"], + configuration["participant"]) return self.callConfiguration diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py index 57c355a..2a29ba0 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -29,7 +29,7 @@ class IncomingCallHandler: _transfer_to_participant_complete_task: asyncio.Future = None _max_retry_attempt_count = 3 - def init(self, calling_server_client: CallingServerClient, call_configuration: CallConfiguration): + 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.targetParticipantpython 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..480d703 --- /dev/null +++ b/IncomingCallRouting/config.ini @@ -0,0 +1,11 @@ +# app settings +[default] +# Configurations related to Communication Service resource +Connectionstring=%Connectionstring% + +BaseUrl= %AppCallBackUri% + +# public url of wav audio +AudioFileUri = %AudioFileUri% +# participant (PhoneNumber/MRI) +TargetParticipant= %Targetpartcipant% \ No newline at end of file diff --git a/IncomingCallRouting/program.py b/IncomingCallRouting/program.py new file mode 100644 index 0000000..75ba4cb --- /dev/null +++ b/IncomingCallRouting/program.py @@ -0,0 +1,17 @@ +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 = CallConfiguration( + config_manager.get_app_settings("Connectionstring"), + config_manager.get_app_settings("BaseUrl"), + config_manager.get_app_settings("AudioFileUri"), + config_manager.get_app_settings("TargetParticipant") + ) + + loop = asyncio.get_event_loop() + loop.run_until_complete(IncomingCallController(config)) + From 47880e34d0c142992a523283a748b3556b4d0619 Mon Sep 17 00:00:00 2001 From: Torres Yang Date: Wed, 5 Jan 2022 17:52:54 -0800 Subject: [PATCH 05/15] Fixed all compilation errors --- .../Controllers/IncomingCallController.py | 12 +- .../EventHandler/EventAuthHandler.py | 2 +- .../EventHandler/EventDispatcher.py | 23 ++-- IncomingCallRouting/Utils/Constants.py | 2 +- .../Utils/IncomingCallHandler.py | 103 +++++++++--------- IncomingCallRouting/config.ini | 1 + IncomingCallRouting/requirements.txt | 3 +- 7 files changed, 76 insertions(+), 70 deletions(-) diff --git a/IncomingCallRouting/Controllers/IncomingCallController.py b/IncomingCallRouting/Controllers/IncomingCallController.py index 79f9b6a..6d68771 100644 --- a/IncomingCallRouting/Controllers/IncomingCallController.py +++ b/IncomingCallRouting/Controllers/IncomingCallController.py @@ -1,18 +1,18 @@ +import json +import ast from typing import List from aiohttp import web from aiohttp.web_routedef import post -from Logger import Logger -import json -import ast -from CallConfiguration import CallConfiguration +from Utils.Logger import Logger +from Utils.CallConfiguration import CallConfiguration from azure.communication.callingserver import CallingServerClient from EventHandler.EventAuthHandler import EventAuthHandler from EventHandler.EventDispatcher import EventDispatcher -from azure.messaging.eventgrid import SystemEvents, EventGridEvent +from azure.core.messaging import CloudEvent from Utils.IncomingCallHandler import IncomingCallHandler -class IncomingCallController(): +class IncomingCallController: app = web.Application() diff --git a/IncomingCallRouting/EventHandler/EventAuthHandler.py b/IncomingCallRouting/EventHandler/EventAuthHandler.py index 0b629b3..191483b 100644 --- a/IncomingCallRouting/EventHandler/EventAuthHandler.py +++ b/IncomingCallRouting/EventHandler/EventAuthHandler.py @@ -1,4 +1,4 @@ -from Logger import Logger +from Utils.Logger import Logger class EventAuthHandler: diff --git a/IncomingCallRouting/EventHandler/EventDispatcher.py b/IncomingCallRouting/EventHandler/EventDispatcher.py index c0d80e7..cf6eecc 100644 --- a/IncomingCallRouting/EventHandler/EventDispatcher.py +++ b/IncomingCallRouting/EventHandler/EventDispatcher.py @@ -1,10 +1,11 @@ -from Logger import Logger -from threading import Lock import threading -from azure.core.messaging import EventGridEvent +from Utils.Logger import Logger +from threading import Lock +from azure.core.messaging import CloudEvent from azure.communication.callingserver import CallingServerEventType, \ CallConnectionStateChangedEvent, ToneReceivedEvent, \ - PlayAudioResultEvent, ParticipantsUpdatedEvent + PlayAudioResultEvent +# ParticipantsUpdatedEvent class EventDispatcher: @@ -38,8 +39,8 @@ def unsubscribe(self, event_type: str, event_key: str): def build_event_key(self, event_type: str, event_key: str): return event_type + "-" + event_key - def process_notification(self, cloudEvent: EventGridEvent): - call_event = self.extract_event(cloudEvent) + 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( @@ -71,7 +72,7 @@ def get_event_key(self, call_event_base): return key return None - def extract_event(self, cloudEvent: EventGridEvent): + def extract_event(self, cloudEvent: CloudEvent): try: if cloudEvent.event_type == CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT: call_connection_state_changed_event = CallConnectionStateChangedEvent.deserialize( @@ -83,10 +84,10 @@ def extract_event(self, cloudEvent: EventGridEvent): cloudEvent.data) return play_audio_result_event - if cloudEvent.event_type == CallingServerEventType.PARTICIPANTS_UPDATED_EVENT: - add_participant_result_event = ParticipantsUpdatedEvent.deserialize( - cloudEvent.data) - return add_participant_result_event + # if cloudEvent.event_type == CallingServerEventType.PARTICIPANTS_UPDATED_EVENT: + # participants_updated_result_event = ParticipantsUpdatedEvent.deserialize( + # cloudEvent.data) + # return participants_updated_result_event if cloudEvent.event_type == CallingServerEventType.TONE_RECEIVED_EVENT: tone_received_event = ToneReceivedEvent.deserialize( diff --git a/IncomingCallRouting/Utils/Constants.py b/IncomingCallRouting/Utils/Constants.py index a5b82ba..161477f 100644 --- a/IncomingCallRouting/Utils/Constants.py +++ b/IncomingCallRouting/Utils/Constants.py @@ -1,7 +1,7 @@ import enum -class Constant(enum.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 index 2a29ba0..71a1072 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -2,24 +2,25 @@ import traceback import uuid import asyncio -import Constants -import CommunicationIdentifierKind -from Logger import Logger -from CommunicationIdentifierKind import CommunicationIdentifierKind +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 CallConfiguration import CallConfiguration -from azure.communication.callingserver.aio import CallingServerClient, CancellationTokenSource, CallConnection, CallConnectionStateChangedEvent, ToneReceivedEvent, ToneInfo, PlayAudioResultEvent, AddParticipantResultEvent, CallMediaType, CallingEventSubscriptionType, CreateCallOptions, CallConnectionState, CallingOperationStatus, ToneValue, PlayAudioOptions, CallingServerEventType, PlayAudioResult, AddParticipantResult +from azure.communication.callingserver.aio import * from azure.communication.callingserver import * from azure.communication.identity._shared.models import * +# #CallingServerClient, CancellationTokenSource, CallConnection, CallConnectionStateChangedEvent, ToneReceivedEvent, ToneInfo, PlayAudioResultEvent, AddParticipantResultEvent, CallMediaType, CallingEventSubscriptionType, CreateCallOptions, CallConnectionState, CallingOperationStatus, ToneValue, PlayAudioOptions, CallingServerEventType, PlayAudioResult, AddParticipantResult PLAY_AUDIO_AWAIT_TIMER = 10 + class IncomingCallHandler: _calling_server_client = None _call_configuration = None _call_connection = None _report_cancellation_token_source = None - _report_cancellation_token = None + _report_cancellation_token = None _target_participant = None _call_estabished_task: asyncio.Future = None @@ -45,20 +46,24 @@ async def report(self, incomming_call_context: str): # answer call response = await self._calling_server_client.answer_call( self._incoming_call_context, - requested_media_types = {CallMediaType.AUDIO}, - requested_call_events = {CallingEventSubscriptionType.PARTICIPANTS_UPDATED, CallingEventSubscriptionType.TONE_RECEIVED}, - callback_uri = self._call_configuration.appCallbackUrl + requested_media_types={CallMediaType.AUDIO}, + requested_call_events={ + CallingEventSubscriptionType.PARTICIPANTS_UPDATED, CallingEventSubscriptionType.TONE_RECEIVED}, + callback_uri=self._call_configuration.appCallbackUrl ) - Logger.log_message(Logger.MessageType.INFORMATION, "AnswerCall Response ----->", response.ToString()) - + Logger.log_message(Logger.MessageType.INFORMATION, + "AnswerCall Response ----->", response.ToString()) + self._call_connection = response.value - register_to_call_state_change_event(self._call_connection.call_connection_id) + register_to_call_state_change_event( + self._call_connection.call_connection_id) # wait for the call to get connected await self._call_estabished_task() - register_to_dtmf_result_event(self._call_connection.callConnectionId) + register_to_dtmf_result_event( + self._call_connection.callConnectionId) await self.play_audio_async() play_audio_completed = await self._play_audio_completed_task @@ -69,41 +74,46 @@ async def report(self, incomming_call_context: str): tone_received_completed_task = await self._tone_received_completed_task() if(tone_received_completed_task == True): participant: str = self._target_participant - Logger.log_message(Logger.MessageType.INFORMATION, "Transfering call to participant ----->", participant) + Logger.log_message( + Logger.MessageType.INFORMATION, "Transfering call to participant ----->", participant) transfer_to_participant_completed = await transfer_to_participant(participant) if(transfer_to_participant_completed == False): await retry_transfer_to_participant_async(participant) await hang_up_async() await self._call_termination_task() except Exception as ex: - Logger.log_message(Logger.MessageType.ERROR, "Call ended unexpectedly, reason:: ", str(ex)) + Logger.log_message(Logger.MessageType.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.MessageType.INFORMATION, "Retrying Transfer participant attempt ", retry_attempt_count, " is in progress") + Logger.log_message(Logger.MessageType.INFORMATION, + "Retrying Transfer participant attempt ", retry_attempt_count, " is in progress") transfer_to_participant_result = await transfer_to_participant(participant) if(transfer_to_participant_result): return else: - Logger.log_message(Logger.MessageType.INFORMATION, "Retrying Transfer participant attempt ", retry_attempt_count, " has failed") + Logger.log_message(Logger.MessageType.INFORMATION, + "Retrying Transfer participant attempt ", 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_url, - is_looped = True, - operation_context = operation_context + audio_url=self._call_configuration.audio_file_url, + is_looped=True, + operation_context=operation_context ) - Logger.log_message(Logger.MesMessageTypes.INFORMATION, "PlayAudioAsync response --> ", response.GetRawResponse(), ", Id: ", response.Value.OperationId, ", Status: ", response.Value.Status, ", OperationContext: ", response.Value.OperationContext, ", ResultInfo: ", response.Value.ResultDetails) + Logger.log_message(Logger.MesMessageTypes.INFORMATION, "PlayAudioAsync response --> ", response.GetRawResponse(), ", Id: ", response.Value.OperationId, + ", Status: ", response.Value.Status, ", OperationContext: ", response.Value.OperationContext, ", ResultInfo: ", response.Value.ResultDetails) if (play_audio_response.Value.Status == CallingOperationStatus.Running): - Logger.log_message(Logger.MessageType.INFORMATION, "Play Audio state: ", response.Value.Status) + Logger.log_message(Logger.MessageType.INFORMATION, + "Play Audio state: ", response.Value.Status) # listen to play audio events self.register_to_play_audio_result_event( play_audio_response.operation_context) @@ -113,7 +123,7 @@ async def play_audio_async(self): tasks.append(asyncio.create_task( asyncio.sleep(PLAY_AUDIO_AWAIT_TIMER))) - await asyncio.wait(tasks, return_when = asyncio.FIRST_COMPLETED) + await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) if (not self.play_audio_completed_task.done()): try: self.play_audio_completed_task.set_result(True) @@ -125,41 +135,36 @@ async def play_audio_async(self): except Exception as ex: pass except TaskCanceledException as tce: - Logger.log_message(Logger.MessageType.ERROR, "Play audio operation cancelled") + Logger.log_message(Logger.MessageType.ERROR, + "Play audio operation cancelled") except Exception as ex: - Logger.log_message(Logger.MessageType.ERROR, "Failure occured while playing audio on the call. Exception: ", str(ex)) + Logger.log_message( + Logger.MessageType.ERROR, "Failure occured while playing audio on the call. Exception: ", str(ex)) async def hang_up_async(self): if(self._report_cancellation_token.isCancellationRequested): - Logger.log_message(Logger.MessageType.INFORMATION, "Cancellation request, Hangup will not be performed") + Logger.log_message(Logger.MessageType.INFORMATION, + "Cancellation request, Hangup will not be performed") return - Logger.log_message(Logger.MessageType.INFORMATION, "Performing Hangup operation") + Logger.log_message(Logger.MessageType.INFORMATION, + "Performing Hangup operation") hang_up_response = await self._call_connection.hang_up_async(self._report_cancellation_token) - Logger.log_message(Logger.MessageType.INFORMATION, "hang_up_async response -----> ", hang_up_response) - + Logger.log_message(Logger.MessageType.INFORMATION, + "hang_up_async response -----> ", hang_up_response) async def cancel_all_media_operations(self): if(self._report_cancellation_token.isCancellationRequested): - Logger.log_message(Logger.MessageType.INFORMATION, "Cancellation request, CancelMediaProcessing will not be performed") + Logger.log_message(Logger.MessageType.INFORMATION, + "Cancellation request, CancelMediaProcessing will not be performed") return - Logger.log_message(Logger.MessageType.INFORMATION, "Cancellation request, CancelMediaProcessing will not be performed") + Logger.log_message(Logger.MessageType.INFORMATION, + "Cancellation request, CancelMediaProcessing will not be performed") operation_context = str(uuid.uuid4()) response = await self._call_connection.CancelAllMediaOperationsAsync(operation_context, self._report_cancellation_token) - Logger.log_message(Logger.MessageType.INFORMATION, "PlayAudioAsync response --> ", response.ContentStream, ", Id: ", response.Content, ", Status: ", response.Status) - - - - - - - - - - - - + Logger.log_message(Logger.MessageType.INFORMATION, "PlayAudioAsync response --> ", + response.ContentStream, ", Id: ", response.Content, ", Status: ", response.Status) def register_to_call_state_change_event(self, call_leg_id): self.call_terminated_task = asyncio.Future() @@ -189,7 +194,6 @@ def call_state_change_notificaiton(call_event): EventDispatcher.get_instance().subscribe(CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT, call_leg_id, call_state_change_notificaiton) - def cancel_media_processing(self): Logger.log_message( Logger.INFORMATION, "Performing cancel media processing operation to stop playing audio") @@ -198,6 +202,7 @@ def cancel_media_processing(self): 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( @@ -220,7 +225,6 @@ def play_prompt_response_notification(call_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_complete_task = asyncio.Future() @@ -251,6 +255,5 @@ def dtmf_received_event(call_event): EventDispatcher.get_instance().subscribe( CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id, dtmf_received_event) - def _get_identifier_kind(participant_number: str): - return CommunicationIdentifierKind.USER_IDENTITY if re.search(Constants.userIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.PHONE_IDENTITY if re.search(Constants.phoneIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.UNKNOWN_IDENTITY + return CommunicationIdentifierKind.USER_IDENTITY if re.search(Constants.userIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.PHONE_IDENTITY if re.search(Constants.phoneIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.UNKNOWN_IDENTITY diff --git a/IncomingCallRouting/config.ini b/IncomingCallRouting/config.ini index 480d703..cb6ef8c 100644 --- a/IncomingCallRouting/config.ini +++ b/IncomingCallRouting/config.ini @@ -7,5 +7,6 @@ BaseUrl= %AppCallBackUri% # public url of wav audio AudioFileUri = %AudioFileUri% + # participant (PhoneNumber/MRI) TargetParticipant= %Targetpartcipant% \ No newline at end of file diff --git a/IncomingCallRouting/requirements.txt b/IncomingCallRouting/requirements.txt index c55b074..4ff72d5 100644 --- a/IncomingCallRouting/requirements.txt +++ b/IncomingCallRouting/requirements.txt @@ -4,9 +4,10 @@ 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-callingserver @ file:///C:\Users\torresyang\Desktop/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 From 60eadf09213e54aa127258a7838bd9a6c64e1654 Mon Sep 17 00:00:00 2001 From: James Deng Date: Thu, 6 Jan 2022 16:40:34 -0800 Subject: [PATCH 06/15] able to hear audio --- .../Controllers/IncomingCallController.py | 39 ++-- .../EventHandler/EventDispatcher.py | 27 +-- .../Utils/CallConfiguration.py | 13 +- .../Utils/IncomingCallHandler.py | 171 ++++++++++-------- IncomingCallRouting/config.ini | 8 +- IncomingCallRouting/program.py | 12 +- IncomingCallRouting/requirements.txt | 3 +- 7 files changed, 149 insertions(+), 124 deletions(-) diff --git a/IncomingCallRouting/Controllers/IncomingCallController.py b/IncomingCallRouting/Controllers/IncomingCallController.py index 6d68771..db99745 100644 --- a/IncomingCallRouting/Controllers/IncomingCallController.py +++ b/IncomingCallRouting/Controllers/IncomingCallController.py @@ -5,7 +5,8 @@ from aiohttp.web_routedef import post from Utils.Logger import Logger from Utils.CallConfiguration import CallConfiguration -from azure.communication.callingserver import CallingServerClient +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 @@ -21,17 +22,17 @@ class IncomingCallController: _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.get( + self.app.add_routes([web.post( '/CallingServerAPICallBacks', self.calling_server_api_callbacks)]) web.run_app(self.app, port=9007) - self._calling_server_client = CallingServerClient( - configuration['ResourceConnectionString']) - self._incoming_calls = [] - self._call_configuration = CallConfiguration.get_call_configuration( - configuration) async def on_incoming_call(self, request): try: @@ -47,29 +48,29 @@ async def on_incoming_call(self, request): code = event_data['validationCode'] if (code): - response_data = {"validationResponse": code} - if(response_data.ValidationResponse != None): + response_data = {"ValidationResponse": code} + if(response_data["ValidationResponse"] != None): return web.Response(body=str(response_data), status=200) - elif (cloud_event.EventType == 'Microsoft.Communication.IncomingCall'): - event_data = str(request) - if(event_data != None): - incoming_call_context = event_data.split( + elif (cloud_event.event_type == 'Microsoft.Communication.IncomingCall'): + if(post_data != None): + 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)) + 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, secret: str): + async def calling_server_api_callbacks(self, request): try: - eventHandler = EventAuthHandler() - if EventAuthHandler.authorize(secret): - if request != None: + 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.MessageType.INFORMATION, "CallingServerAPICallBacks-------> {request.ToString()}") + Logger.INFORMATION, "CallingServerAPICallBacks -------> " + str(request)) eventDispatcher: EventDispatcher = EventDispatcher.get_instance() eventDispatcher.process_notification( str(http_content.decode('UTF-8'))) diff --git a/IncomingCallRouting/EventHandler/EventDispatcher.py b/IncomingCallRouting/EventHandler/EventDispatcher.py index cf6eecc..db42f3c 100644 --- a/IncomingCallRouting/EventHandler/EventDispatcher.py +++ b/IncomingCallRouting/EventHandler/EventDispatcher.py @@ -1,11 +1,11 @@ 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 -# ParticipantsUpdatedEvent + PlayAudioResultEvent, ParticipantsUpdatedEvent class EventDispatcher: @@ -72,26 +72,27 @@ def get_event_key(self, call_event_base): return key return None - def extract_event(self, cloudEvent: CloudEvent): + def extract_event(self, request: str): try: - if cloudEvent.event_type == CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT: + event = CloudEvent.from_dict(json.loads(request)[0]) + if event.type == CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT: call_connection_state_changed_event = CallConnectionStateChangedEvent.deserialize( - cloudEvent.data) + event.data) return call_connection_state_changed_event - if cloudEvent.event_type == CallingServerEventType.PLAY_AUDIO_RESULT_EVENT: + if event.type == CallingServerEventType.PLAY_AUDIO_RESULT_EVENT: play_audio_result_event = PlayAudioResultEvent.deserialize( - cloudEvent.data) + event.data) return play_audio_result_event - # if cloudEvent.event_type == CallingServerEventType.PARTICIPANTS_UPDATED_EVENT: - # participants_updated_result_event = ParticipantsUpdatedEvent.deserialize( - # cloudEvent.data) - # return participants_updated_result_event + if event.type == CallingServerEventType.PARTICIPANTS_UPDATED_EVENT: + participants_updated_result_event = ParticipantsUpdatedEvent.deserialize( + event.data) + return participants_updated_result_event - if cloudEvent.event_type == CallingServerEventType.TONE_RECEIVED_EVENT: + if event.type == CallingServerEventType.TONE_RECEIVED_EVENT: tone_received_event = ToneReceivedEvent.deserialize( - cloudEvent.data) + event.data) return tone_received_event except Exception as ex: diff --git a/IncomingCallRouting/Utils/CallConfiguration.py b/IncomingCallRouting/Utils/CallConfiguration.py index aa92827..aece116 100644 --- a/IncomingCallRouting/Utils/CallConfiguration.py +++ b/IncomingCallRouting/Utils/CallConfiguration.py @@ -11,14 +11,15 @@ def __init__(self, connection_string, app_base_url, audio_file_uri, participant) eventhandler = EventAuthHandler() self.app_callback_url: str = app_base_url + \ "/CallingServerAPICallBacks?" + eventhandler.get_secret_querystring() - self.targetParticipant: str = str(participant) + self.target_participant: str = str(participant) - def get_call_configuration(self, configuration): - if(self.callConfiguration != None): - self.callConfiguration = CallConfiguration( + @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["participant"]) + configuration["target_participant"]) - return self.callConfiguration + return cls.callConfiguration diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py index 71a1072..0222873 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -19,11 +19,9 @@ class IncomingCallHandler: _calling_server_client = None _call_configuration = None _call_connection = None - _report_cancellation_token_source = None - _report_cancellation_token = None _target_participant = None - _call_estabished_task: asyncio.Future = None + _call_established_task: asyncio.Future = None _play_audio_completed_task: asyncio.Future = None _call_terminatied_task: asyncio.Future = None _tone_received_completed_task: asyncio.Future = None @@ -33,89 +31,86 @@ class IncomingCallHandler: 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.targetParticipantpython + self._target_participant = call_configuration.target_participant async def report(self, incomming_call_context: str): - self._report_cancellation_token_source = CancellationTokenSource() - self._report_cancellation_token = self._report_cancellation_token_source.Token - try: # wait for 10 sec before answering the call. - await asyncio.sleep(10 * 1000) + await asyncio.sleep(10) # answer call response = await self._calling_server_client.answer_call( - self._incoming_call_context, + incomming_call_context, requested_media_types={CallMediaType.AUDIO}, requested_call_events={ CallingEventSubscriptionType.PARTICIPANTS_UPDATED, CallingEventSubscriptionType.TONE_RECEIVED}, - callback_uri=self._call_configuration.appCallbackUrl + callback_uri=self._call_configuration.app_callback_url ) - Logger.log_message(Logger.MessageType.INFORMATION, - "AnswerCall Response ----->", response.ToString()) + Logger.log_message(Logger.INFORMATION, + "AnswerCall Response ----->" + str(response)) - self._call_connection = response.value - register_to_call_state_change_event( + 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_estabished_task() + await self._call_established_task - register_to_dtmf_result_event( - self._call_connection.callConnectionId) + self._register_to_dtmf_result_event( + self._call_connection.call_connection_id) - await self.play_audio_async() + await self._play_audio_async() play_audio_completed = await self._play_audio_completed_task if(play_audio_completed == False): - await hang_up_async() + await self._hang_up_async() else: tone_received_completed_task = await self._tone_received_completed_task() if(tone_received_completed_task == True): participant: str = self._target_participant Logger.log_message( - Logger.MessageType.INFORMATION, "Transfering call to participant ----->", participant) - transfer_to_participant_completed = await transfer_to_participant(participant) + Logger.INFORMATION, "Transfering call to participant -----> " + participant) + transfer_to_participant_completed = await self._transfer_to_participant(participant) if(transfer_to_participant_completed == False): - await retry_transfer_to_participant_async(participant) - await hang_up_async() + await self._retry_transfer_to_participant_async(participant) + await self._hang_up_async() await self._call_termination_task() except Exception as ex: - Logger.log_message(Logger.MessageType.ERROR, - "Call ended unexpectedly, reason:: ", str(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): + 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.MessageType.INFORMATION, - "Retrying Transfer participant attempt ", retry_attempt_count, " is in progress") - transfer_to_participant_result = await transfer_to_participant(participant) + 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.MessageType.INFORMATION, - "Retrying Transfer participant attempt ", retry_attempt_count, " has failed") + 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): + 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_url, + audio_url=self._call_configuration.audio_file_uri, is_looped=True, operation_context=operation_context ) - Logger.log_message(Logger.MesMessageTypes.INFORMATION, "PlayAudioAsync response --> ", response.GetRawResponse(), ", Id: ", response.Value.OperationId, - ", Status: ", response.Value.Status, ", OperationContext: ", response.Value.OperationContext, ", ResultInfo: ", response.Value.ResultDetails) + Logger.log_message(Logger.INFORMATION, "PlayAudioAsync response --> " + play_audio_response.GetRawResponse() + ", Id: " + play_audio_response.Value.OperationId + + ", Status: " + play_audio_response.Value.Status + ", OperationContext: " + str(play_audio_response.Value.OperationContext), ", ResultInfo: " + play_audio_response.Value.ResultDetails) - if (play_audio_response.Value.Status == CallingOperationStatus.Running): - Logger.log_message(Logger.MessageType.INFORMATION, - "Play Audio state: ", response.Value.Status) + if (play_audio_response.Status == CallingOperationStatus.Running): + Logger.log_message(Logger.INFORMATION, + "Play Audio state: " + play_audio_response.Value.Status) # listen to play audio events - self.register_to_play_audio_result_event( + self._register_to_play_audio_result_event( play_audio_response.operation_context) tasks = [] @@ -134,41 +129,32 @@ async def play_audio_async(self): self.tone_received_complete_task.set_result(True) except Exception as ex: pass - except TaskCanceledException as tce: - Logger.log_message(Logger.MessageType.ERROR, - "Play audio operation cancelled") except Exception as ex: Logger.log_message( - Logger.MessageType.ERROR, "Failure occured while playing audio on the call. Exception: ", str(ex)) + Logger.ERROR, "Failure occured while playing audio on the call. Exception: " + str(ex)) - async def hang_up_async(self): - if(self._report_cancellation_token.isCancellationRequested): - Logger.log_message(Logger.MessageType.INFORMATION, - "Cancellation request, Hangup will not be performed") - return - Logger.log_message(Logger.MessageType.INFORMATION, + async def _hang_up_async(self): + Logger.log_message(Logger.INFORMATION, "Performing Hangup operation") - hang_up_response = await self._call_connection.hang_up_async(self._report_cancellation_token) - Logger.log_message(Logger.MessageType.INFORMATION, - "hang_up_async response -----> ", hang_up_response) + hang_up_response = await self._call_connection.hang_up() + Logger.log_message(Logger.INFORMATION, + "hang_up_async response -----> " + hang_up_response) - async def cancel_all_media_operations(self): + async def _cancel_all_media_operations(self): if(self._report_cancellation_token.isCancellationRequested): - Logger.log_message(Logger.MessageType.INFORMATION, + Logger.log_message(Logger.INFORMATION, "Cancellation request, CancelMediaProcessing will not be performed") return - Logger.log_message(Logger.MessageType.INFORMATION, + Logger.log_message(Logger.INFORMATION, "Cancellation request, CancelMediaProcessing will not be performed") + response = await self._call_connection.cancel_all_media_operations() - operation_context = str(uuid.uuid4()) - response = await self._call_connection.CancelAllMediaOperationsAsync(operation_context, self._report_cancellation_token) - - Logger.log_message(Logger.MessageType.INFORMATION, "PlayAudioAsync response --> ", - response.ContentStream, ", Id: ", response.Content, ", Status: ", response.Status) + Logger.log_message(Logger.INFORMATION, "PlayAudioAsync response --> " + + response.ContentStream + ", Id: " + response.Content + ", Status: " + response.Status) - def register_to_call_state_change_event(self, call_leg_id): - self.call_terminated_task = asyncio.Future() - self.call_connected_task = asyncio.Future() + 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): @@ -180,12 +166,12 @@ def call_state_change_notificaiton(call_event): if (call_state_changes.call_connection_state == CallConnectionState.CONNECTED): Logger.log_message(Logger.INFORMATION, "Call State successfully connected") - self.call_connected_task.set_result(True) + 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) + self._call_terminated_task.set_result(True) except asyncio.InvalidStateError: pass @@ -194,13 +180,7 @@ def call_state_change_notificaiton(call_event): EventDispatcher.get_instance().subscribe(CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT, call_leg_id, call_state_change_notificaiton) - def cancel_media_processing(self): - Logger.log_message( - Logger.INFORMATION, "Performing cancel media processing operation to stop playing audio") - - self.call_connection.cancel_all_media_operations() - - def register_to_play_audio_result_event(self, operation_context): + def _register_to_play_audio_result_event(self, operation_context): self._play_audio_completed_task = asyncio.Future() def play_prompt_response_notification(call_event): @@ -225,7 +205,7 @@ def play_prompt_response_notification(call_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): + def _register_to_dtmf_result_event(self, call_leg_id): self.tone_received_complete_task = asyncio.Future() def dtmf_received_event(call_event): @@ -249,11 +229,52 @@ def dtmf_received_event(call_event): EventDispatcher.get_instance().unsubscribe( CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id) # cancel playing audio - self.cancel_media_processing() + self._cancel_all_media_operations() # 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, "transfered call to temp") + 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, "transfered call to temp") + + transfer_to_participant_completed = await self._transfer_to_participant_complete_task + return transfer_to_participant_completed + + + async def _register_to_transfer_participants_result_event(self, operation_context: str): + async def transfer_to_participant_received_event(call_event): + transfer_to_participant_updated_event: ParticipantsUpdatedEvent = call_event + if(transfer_to_participant_updated_event != None): + Logger.log_message(Logger.INFORMATION, "Transfer participant callconnection ID - " + transfer_to_participant_updated_event.CallConnectionId) + EventDispatcher.get_instance().unsubscribe(CallingServerEventType.ParticipantsUpdatedEvent, operation_context) + Logger.log_message(Logger.INFORMATION, "Sleeping for 60 seconds before proceeding further") + await asyncio.sleep(60) + self._transfer_to_participant_complete_task.set_result(True) + else: + self._transfer_to_participant_complete_task.set_result(False) + + EventDispatcher.get_instance().unsubscribe(CallingServerEventType.ParticipantsUpdatedEvent, operation_context, transfer_to_participant_received_event) + + def _get_identifier_kind(participant_number: str): - return CommunicationIdentifierKind.USER_IDENTITY if re.search(Constants.userIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.PHONE_IDENTITY if re.search(Constants.phoneIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.UNKNOWN_IDENTITY + return CommunicationIdentifierKind.USER_IDENTITY if re.search(Constants.userIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.PHONE_IDENTITY if re.search(Constants.phoneIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.UNKNOWN_IDENTITY \ No newline at end of file diff --git a/IncomingCallRouting/config.ini b/IncomingCallRouting/config.ini index cb6ef8c..9bdfa75 100644 --- a/IncomingCallRouting/config.ini +++ b/IncomingCallRouting/config.ini @@ -1,12 +1,12 @@ # app settings [default] # Configurations related to Communication Service resource -Connectionstring=%Connectionstring% +Connectionstring=endpoint=https://acs-test-james.communication.azure.com/;accesskey=27Tb7wlidbeaeV36lVyjO1P4N1tX782Jty3NeUjKKEe30csRFqPGX9tG9kyjOI3nhm8StnGo9K6bwVSudGAOTw== -BaseUrl= %AppCallBackUri% +BaseUrl= https://7289-2001-569-7201-4400-3972-3f80-e3bd-7045.ngrok.io # public url of wav audio -AudioFileUri = %AudioFileUri% +AudioFileUri = https://acstestapp1.azurewebsites.net/audio/bot-hold-music-2.wav # participant (PhoneNumber/MRI) -TargetParticipant= %Targetpartcipant% \ No newline at end of file +TargetParticipant= 8:acs:3afbe310-c6d9-4b6f-a11e-c2aeb352f207_0000000e-d06a-18d5-f40f-343a0d00138c \ No newline at end of file diff --git a/IncomingCallRouting/program.py b/IncomingCallRouting/program.py index 75ba4cb..af53784 100644 --- a/IncomingCallRouting/program.py +++ b/IncomingCallRouting/program.py @@ -5,12 +5,12 @@ if __name__ == '__main__': config_manager = ConfigurationManager.get_instance() - config = CallConfiguration( - config_manager.get_app_settings("Connectionstring"), - config_manager.get_app_settings("BaseUrl"), - config_manager.get_app_settings("AudioFileUri"), - config_manager.get_app_settings("TargetParticipant") - ) + 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") + } loop = asyncio.get_event_loop() loop.run_until_complete(IncomingCallController(config)) diff --git a/IncomingCallRouting/requirements.txt b/IncomingCallRouting/requirements.txt index 4ff72d5..9868876 100644 --- a/IncomingCallRouting/requirements.txt +++ b/IncomingCallRouting/requirements.txt @@ -4,13 +4,14 @@ 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:\Users\torresyang\Desktop/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 From b82da73edc57699c35c5a59622b4c66bda7e7736 Mon Sep 17 00:00:00 2001 From: James Deng Date: Thu, 6 Jan 2022 17:21:17 -0800 Subject: [PATCH 07/15] remove key --- .../Utils/IncomingCallHandler.py | 26 +++++++++---------- IncomingCallRouting/config.ini | 8 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py index 0222873..048a066 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -66,7 +66,7 @@ async def report(self, incomming_call_context: str): if(play_audio_completed == False): await self._hang_up_async() else: - tone_received_completed_task = await self._tone_received_completed_task() + tone_received_completed_task = await self._tone_received_completed_task if(tone_received_completed_task == True): participant: str = self._target_participant Logger.log_message( @@ -103,25 +103,25 @@ async def _play_audio_async(self): is_looped=True, operation_context=operation_context ) - Logger.log_message(Logger.INFORMATION, "PlayAudioAsync response --> " + play_audio_response.GetRawResponse() + ", Id: " + play_audio_response.Value.OperationId + - ", Status: " + play_audio_response.Value.Status + ", OperationContext: " + str(play_audio_response.Value.OperationContext), ", ResultInfo: " + play_audio_response.Value.ResultDetails) + 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): + if (play_audio_response.status == CallingOperationStatus.RUNNING): Logger.log_message(Logger.INFORMATION, - "Play Audio state: " + play_audio_response.Value.Status) + "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(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()): + if (not self._play_audio_completed_task.done()): try: - self.play_audio_completed_task.set_result(True) + self._play_audio_completed_task.set_result(True) except Exception as ex: pass try: @@ -150,7 +150,7 @@ async def _cancel_all_media_operations(self): response = await self._call_connection.cancel_all_media_operations() Logger.log_message(Logger.INFORMATION, "PlayAudioAsync response --> " + - response.ContentStream + ", Id: " + response.Content + ", Status: " + response.Status) + response.content_stream + ", Id: " + response.content + ", Status: " + response.status) def _register_to_call_state_change_event(self, call_leg_id): self._call_terminated_task = asyncio.Future() @@ -192,12 +192,12 @@ def play_prompt_response_notification(call_event): EventDispatcher.get_instance().unsubscribe( CallingServerEventType.PLAY_AUDIO_RESULT_EVENT, operation_context) try: - self.play_audio_completed_task.set_result(True) + 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) + self._play_audio_completed_task.set_result(False) except: pass @@ -265,8 +265,8 @@ async def _register_to_transfer_participants_result_event(self, operation_contex async def transfer_to_participant_received_event(call_event): transfer_to_participant_updated_event: ParticipantsUpdatedEvent = call_event if(transfer_to_participant_updated_event != None): - Logger.log_message(Logger.INFORMATION, "Transfer participant callconnection ID - " + transfer_to_participant_updated_event.CallConnectionId) - EventDispatcher.get_instance().unsubscribe(CallingServerEventType.ParticipantsUpdatedEvent, operation_context) + Logger.log_message(Logger.INFORMATION, "Transfer participant callconnection ID - " + transfer_to_participant_updated_event.call_connection_id) + EventDispatcher.get_instance().unsubscribe(CallingServerEventType.PARTICIPANTS_UPDATED_EVENT, operation_context) Logger.log_message(Logger.INFORMATION, "Sleeping for 60 seconds before proceeding further") await asyncio.sleep(60) self._transfer_to_participant_complete_task.set_result(True) diff --git a/IncomingCallRouting/config.ini b/IncomingCallRouting/config.ini index 9bdfa75..e447415 100644 --- a/IncomingCallRouting/config.ini +++ b/IncomingCallRouting/config.ini @@ -1,12 +1,12 @@ # app settings [default] # Configurations related to Communication Service resource -Connectionstring=endpoint=https://acs-test-james.communication.azure.com/;accesskey=27Tb7wlidbeaeV36lVyjO1P4N1tX782Jty3NeUjKKEe30csRFqPGX9tG9kyjOI3nhm8StnGo9K6bwVSudGAOTw== +Connectionstring=%Connectionstring% -BaseUrl= https://7289-2001-569-7201-4400-3972-3f80-e3bd-7045.ngrok.io +BaseUrl= %BaseUrl% # public url of wav audio -AudioFileUri = https://acstestapp1.azurewebsites.net/audio/bot-hold-music-2.wav +AudioFileUri = %AudioFileUri% # participant (PhoneNumber/MRI) -TargetParticipant= 8:acs:3afbe310-c6d9-4b6f-a11e-c2aeb352f207_0000000e-d06a-18d5-f40f-343a0d00138c \ No newline at end of file +TargetParticipant= %TargetParticipant% \ No newline at end of file From 671ba75fcbaeaec4a164489d335502f115497235 Mon Sep 17 00:00:00 2001 From: James Deng Date: Thu, 6 Jan 2022 19:34:13 -0800 Subject: [PATCH 08/15] make it able to transfer call --- .../Utils/CommunicationIdentifierKind.py | 6 ++--- IncomingCallRouting/Utils/Constants.py | 4 +-- .../Utils/IncomingCallHandler.py | 25 +++++++++++-------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/IncomingCallRouting/Utils/CommunicationIdentifierKind.py b/IncomingCallRouting/Utils/CommunicationIdentifierKind.py index 571b625..8fc375e 100644 --- a/IncomingCallRouting/Utils/CommunicationIdentifierKind.py +++ b/IncomingCallRouting/Utils/CommunicationIdentifierKind.py @@ -3,6 +3,6 @@ class CommunicationIdentifierKind(enum.Enum): - USER_IDENTITY = 1 - PHONE_IDENTITY = 2 - UNKNOWN_IDENTITY = 3 + USER_IDENTITY = "USER_IDENTITY" + PHONE_IDENTITY = "PHONE_IDENTITY" + UNKNOWN_IDENTITY = "UNKNOWN_IDENTITY" diff --git a/IncomingCallRouting/Utils/Constants.py b/IncomingCallRouting/Utils/Constants.py index 161477f..3efb2c2 100644 --- a/IncomingCallRouting/Utils/Constants.py +++ b/IncomingCallRouting/Utils/Constants.py @@ -3,5 +3,5 @@ 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}$" + 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 index 048a066..51a0094 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -7,11 +7,10 @@ from Utils.CommunicationIdentifierKind import CommunicationIdentifierKind from Utils.CallConfiguration import CallConfiguration from EventHandler.EventDispatcher import EventDispatcher -from azure.communication.callingserver.aio import * -from azure.communication.callingserver import * -from azure.communication.identity._shared.models import * +from azure.communication.callingserver.aio import CallingServerClient +from azure.communication.callingserver import CallConnectionStateChangedEvent, ToneReceivedEvent, ToneInfo, PlayAudioResultEvent, CallMediaType, CallingEventSubscriptionType, CallConnectionState, CallingOperationStatus, ToneValue, CallingServerEventType, ParticipantsUpdatedEvent, CommunicationUserIdentifier, PhoneNumberIdentifier +# from azure.communication.identity import -# #CallingServerClient, CancellationTokenSource, CallConnection, CallConnectionStateChangedEvent, ToneReceivedEvent, ToneInfo, PlayAudioResultEvent, AddParticipantResultEvent, CallMediaType, CallingEventSubscriptionType, CreateCallOptions, CallConnectionState, CallingOperationStatus, ToneValue, PlayAudioOptions, CallingServerEventType, PlayAudioResult, AddParticipantResult PLAY_AUDIO_AWAIT_TIMER = 10 @@ -126,7 +125,7 @@ async def _play_audio_async(self): pass try: # After playing audio for 10 sec, make toneReceivedCompleteTask true. - self.tone_received_complete_task.set_result(True) + self._tone_received_completed_task.set_result(True) except Exception as ex: pass except Exception as ex: @@ -206,7 +205,7 @@ def play_prompt_response_notification(call_event): operation_context, play_prompt_response_notification) def _register_to_dtmf_result_event(self, call_leg_id): - self.tone_received_complete_task = asyncio.Future() + self._tone_received_completed_task = asyncio.Future() def dtmf_received_event(call_event): tone_received_event: ToneReceivedEvent = call_event @@ -217,12 +216,12 @@ def dtmf_received_event(call_event): if (tone_info.tone == ToneValue.TONE1): try: - self.tone_received_complete_task.set_result(True) + self._tone_received_completed_task.set_result(True) except: pass else: try: - self.tone_received_complete_task.set_result(False) + self._tone_received_completed_task.set_result(False) except: pass @@ -276,5 +275,11 @@ async def transfer_to_participant_received_event(call_event): EventDispatcher.get_instance().unsubscribe(CallingServerEventType.ParticipantsUpdatedEvent, operation_context, transfer_to_participant_received_event) - def _get_identifier_kind(participant_number: str): - return CommunicationIdentifierKind.USER_IDENTITY if re.search(Constants.userIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.PHONE_IDENTITY if re.search(Constants.phoneIdentityRegex, participant_number, re.IGNORECASE) else CommunicationIdentifierKind.UNKNOWN_IDENTITY \ No newline at end of file + def _get_identifier_kind(self, participant_number: str): + # if(re.search(Constants.userIdentityRegex.value, participant_number, re.IGNORECASE)): + # return CommunicationIdentifierKind.USER_IDENTITY + # elif(re.search(Constants.phoneIdentityRegex.value, participant_number, re.IGNORECASE)): + # return CommunicationIdentifierKind.PHONE_IDENTITY + # else: + # return CommunicationIdentifierKind.UNKNOWN_IDENTITY + 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 From 5f1ded3757e987b34d8e30989bd7c83906ee5e8e Mon Sep 17 00:00:00 2001 From: James Deng Date: Fri, 7 Jan 2022 09:36:06 -0800 Subject: [PATCH 09/15] call cancel all media when set play audio complete to true --- .../Utils/IncomingCallHandler.py | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py index 51a0094..df4b92f 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -74,7 +74,7 @@ async def report(self, incomming_call_context: str): if(transfer_to_participant_completed == False): await self._retry_transfer_to_participant_async(participant) await self._hang_up_async() - await self._call_termination_task() + await self._call_terminatied_task except Exception as ex: Logger.log_message(Logger.ERROR, "Call ended unexpectedly, reason: " + str(ex)) @@ -120,6 +120,8 @@ async def _play_audio_async(self): 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(True) except Exception as ex: pass @@ -140,16 +142,9 @@ async def _hang_up_async(self): "hang_up_async response -----> " + hang_up_response) async def _cancel_all_media_operations(self): - if(self._report_cancellation_token.isCancellationRequested): - Logger.log_message(Logger.INFORMATION, - "Cancellation request, CancelMediaProcessing will not be performed") - return Logger.log_message(Logger.INFORMATION, - "Cancellation request, CancelMediaProcessing will not be performed") - response = await self._call_connection.cancel_all_media_operations() - - Logger.log_message(Logger.INFORMATION, "PlayAudioAsync response --> " + - response.content_stream + ", Id: " + response.content + ", Status: " + response.status) + "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() @@ -204,10 +199,10 @@ def play_prompt_response_notification(call_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): + async def _register_to_dtmf_result_event(self, call_leg_id): self._tone_received_completed_task = asyncio.Future() - def dtmf_received_event(call_event): + async def dtmf_received_event(call_event): tone_received_event: ToneReceivedEvent = call_event tone_info: ToneInfo = tone_received_event.tone_info @@ -228,7 +223,7 @@ def dtmf_received_event(call_event): EventDispatcher.get_instance().unsubscribe( CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id) # cancel playing audio - self._cancel_all_media_operations() + await self._cancel_all_media_operations() # Subscribe to event EventDispatcher.get_instance().subscribe( @@ -250,11 +245,12 @@ async def _transfer_to_participant(self, target_participant: str): 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, "transfered call to temp") + 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, "transfered call to temp") + 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 @@ -276,10 +272,4 @@ async def transfer_to_participant_received_event(call_event): def _get_identifier_kind(self, participant_number: str): - # if(re.search(Constants.userIdentityRegex.value, participant_number, re.IGNORECASE)): - # return CommunicationIdentifierKind.USER_IDENTITY - # elif(re.search(Constants.phoneIdentityRegex.value, participant_number, re.IGNORECASE)): - # return CommunicationIdentifierKind.PHONE_IDENTITY - # else: - # return CommunicationIdentifierKind.UNKNOWN_IDENTITY 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 From ac14a71ca4ebbabe2691eb5d52ddb8c8274e260d Mon Sep 17 00:00:00 2001 From: James Deng Date: Fri, 7 Jan 2022 14:25:10 -0800 Subject: [PATCH 10/15] check to id before handling the call --- .../Controllers/IncomingCallController.py | 2 +- IncomingCallRouting/Utils/CallConfiguration.py | 6 ++++-- IncomingCallRouting/Utils/IncomingCallHandler.py | 3 +-- IncomingCallRouting/config.ini | 9 ++++++--- IncomingCallRouting/program.py | 3 ++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/IncomingCallRouting/Controllers/IncomingCallController.py b/IncomingCallRouting/Controllers/IncomingCallController.py index db99745..f18b331 100644 --- a/IncomingCallRouting/Controllers/IncomingCallController.py +++ b/IncomingCallRouting/Controllers/IncomingCallController.py @@ -52,7 +52,7 @@ async def on_incoming_call(self, request): 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): + 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)) diff --git a/IncomingCallRouting/Utils/CallConfiguration.py b/IncomingCallRouting/Utils/CallConfiguration.py index aece116..35a3972 100644 --- a/IncomingCallRouting/Utils/CallConfiguration.py +++ b/IncomingCallRouting/Utils/CallConfiguration.py @@ -4,7 +4,7 @@ class CallConfiguration: callConfiguration = None - def __init__(self, connection_string, app_base_url, audio_file_uri, participant): + 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) @@ -12,6 +12,7 @@ def __init__(self, connection_string, app_base_url, audio_file_uri, participant) 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): @@ -20,6 +21,7 @@ def get_call_configuration(cls, configuration): configuration["connection_string"], configuration["app_base_url"], configuration["audio_file_uri"], - configuration["target_participant"]) + configuration["target_participant"], + configuration["bot_identity"]) return cls.callConfiguration diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py index df4b92f..2ba69a7 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -199,7 +199,7 @@ def play_prompt_response_notification(call_event): EventDispatcher.get_instance().subscribe(CallingServerEventType.PLAY_AUDIO_RESULT_EVENT, operation_context, play_prompt_response_notification) - async def _register_to_dtmf_result_event(self, call_leg_id): + def _register_to_dtmf_result_event(self, call_leg_id): self._tone_received_completed_task = asyncio.Future() async def dtmf_received_event(call_event): @@ -219,7 +219,6 @@ async def dtmf_received_event(call_event): self._tone_received_completed_task.set_result(False) except: pass - EventDispatcher.get_instance().unsubscribe( CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id) # cancel playing audio diff --git a/IncomingCallRouting/config.ini b/IncomingCallRouting/config.ini index e447415..2a8da8b 100644 --- a/IncomingCallRouting/config.ini +++ b/IncomingCallRouting/config.ini @@ -3,10 +3,13 @@ # Configurations related to Communication Service resource Connectionstring=%Connectionstring% -BaseUrl= %BaseUrl% +BaseUrl=%BaseUrl% # public url of wav audio -AudioFileUri = %AudioFileUri% +AudioFileUri=%AudioFileUri% # participant (PhoneNumber/MRI) -TargetParticipant= %TargetParticipant% \ No newline at end of file +TargetParticipant=%TargetParticipant% + +# identity for the bot +BotIdentity=%BotIdentity% \ No newline at end of file diff --git a/IncomingCallRouting/program.py b/IncomingCallRouting/program.py index af53784..dc72da7 100644 --- a/IncomingCallRouting/program.py +++ b/IncomingCallRouting/program.py @@ -9,7 +9,8 @@ "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") + "target_participant": config_manager.get_app_settings("TargetParticipant"), + "bot_identity": config_manager.get_app_settings("BotIdentity") } loop = asyncio.get_event_loop() From 36821e75bd8452b62786b5f818d3e4bc37f46e70 Mon Sep 17 00:00:00 2001 From: James Deng Date: Tue, 11 Jan 2022 15:56:30 -0800 Subject: [PATCH 11/15] use Transfer_Call_Result_Event instead of Participants_Updated_Event --- .../EventHandler/EventDispatcher.py | 14 +++++------ .../Utils/IncomingCallHandler.py | 24 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/IncomingCallRouting/EventHandler/EventDispatcher.py b/IncomingCallRouting/EventHandler/EventDispatcher.py index db42f3c..53a88d7 100644 --- a/IncomingCallRouting/EventHandler/EventDispatcher.py +++ b/IncomingCallRouting/EventHandler/EventDispatcher.py @@ -5,7 +5,7 @@ from azure.core.messaging import CloudEvent from azure.communication.callingserver import CallingServerEventType, \ CallConnectionStateChangedEvent, ToneReceivedEvent, \ - PlayAudioResultEvent, ParticipantsUpdatedEvent + PlayAudioResultEvent, TransferCallResultEvent class EventDispatcher: @@ -65,10 +65,10 @@ def get_event_key(self, call_event_base): key = self.build_event_key( CallingServerEventType.PLAY_AUDIO_RESULT_EVENT, operation_context) return key - elif type(call_event_base) == ParticipantsUpdatedEvent: - call_leg_id = call_event_base.call_connection_id + elif type(call_event_base) == TransferCallResultEvent: + call_leg_id = call_event_base.operation_context key = self.build_event_key( - CallingServerEventType.PARTICIPANTS_UPDATED_EVENT, call_leg_id) + CallingServerEventType.TRANSFER_CALL_RESULT_EVENT, call_leg_id) return key return None @@ -85,10 +85,10 @@ def extract_event(self, request: str): event.data) return play_audio_result_event - if event.type == CallingServerEventType.PARTICIPANTS_UPDATED_EVENT: - participants_updated_result_event = ParticipantsUpdatedEvent.deserialize( + if event.type == CallingServerEventType.TRANSFER_CALL_RESULT_EVENT: + transfer_call_result_event = TransferCallResultEvent.deserialize( event.data) - return participants_updated_result_event + return transfer_call_result_event if event.type == CallingServerEventType.TONE_RECEIVED_EVENT: tone_received_event = ToneReceivedEvent.deserialize( diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py index 2ba69a7..843bbb0 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -8,7 +8,7 @@ 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, ParticipantsUpdatedEvent, CommunicationUserIdentifier, PhoneNumberIdentifier +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 @@ -22,7 +22,6 @@ class IncomingCallHandler: _call_established_task: asyncio.Future = None _play_audio_completed_task: asyncio.Future = None - _call_terminatied_task: asyncio.Future = None _tone_received_completed_task: asyncio.Future = None _transfer_to_participant_complete_task: asyncio.Future = None _max_retry_attempt_count = 3 @@ -66,6 +65,7 @@ async def report(self, incomming_call_context: str): 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( @@ -73,8 +73,8 @@ async def report(self, incomming_call_context: str): transfer_to_participant_completed = await self._transfer_to_participant(participant) if(transfer_to_participant_completed == False): await self._retry_transfer_to_participant_async(participant) - await self._hang_up_async() - await self._call_terminatied_task + 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)) @@ -255,19 +255,17 @@ async def _transfer_to_participant(self, target_participant: str): return transfer_to_participant_completed - async def _register_to_transfer_participants_result_event(self, operation_context: str): - async def transfer_to_participant_received_event(call_event): - transfer_to_participant_updated_event: ParticipantsUpdatedEvent = call_event - if(transfer_to_participant_updated_event != None): - Logger.log_message(Logger.INFORMATION, "Transfer participant callconnection ID - " + transfer_to_participant_updated_event.call_connection_id) - EventDispatcher.get_instance().unsubscribe(CallingServerEventType.PARTICIPANTS_UPDATED_EVENT, operation_context) - Logger.log_message(Logger.INFORMATION, "Sleeping for 60 seconds before proceeding further") - await asyncio.sleep(60) + 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().unsubscribe(CallingServerEventType.ParticipantsUpdatedEvent, operation_context, transfer_to_participant_received_event) + EventDispatcher.get_instance().subscribe(CallingServerEventType.TRANSFER_CALL_RESULT_EVENT, operation_context, transfer_to_participant_received_event) def _get_identifier_kind(self, participant_number: str): From cc082da35be0b58f1952750520683439e3c85992 Mon Sep 17 00:00:00 2001 From: Torres Yang Date: Wed, 12 Jan 2022 16:19:48 -0800 Subject: [PATCH 12/15] Added readme for incoming call routing sample --- IncomingCallRouting/readme.md | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 IncomingCallRouting/readme.md 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 From b64c59029e9c2e5350dc5898cc14bcc722ed8d8c Mon Sep 17 00:00:00 2001 From: Torres Yang Date: Wed, 12 Jan 2022 16:59:02 -0800 Subject: [PATCH 13/15] Delete .vscode directory --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 157d539..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.analysis.extraPaths": ["./IncomingCallRouting/Utils"] -} From 72525c2646a0e048989d28675560bad8d2392182 Mon Sep 17 00:00:00 2001 From: James Deng Date: Fri, 14 Jan 2022 13:41:24 -0800 Subject: [PATCH 14/15] handle dtmf event --- .../EventHandler/EventDispatcher.py | 2 ++ .../Utils/IncomingCallHandler.py | 28 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/IncomingCallRouting/EventHandler/EventDispatcher.py b/IncomingCallRouting/EventHandler/EventDispatcher.py index 53a88d7..b4ca008 100644 --- a/IncomingCallRouting/EventHandler/EventDispatcher.py +++ b/IncomingCallRouting/EventHandler/EventDispatcher.py @@ -46,6 +46,7 @@ def process_notification(self, request: str): 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() @@ -75,6 +76,7 @@ def get_event_key(self, call_event_base): 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) diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py index 843bbb0..9e32add 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -33,9 +33,6 @@ def __init__(self, calling_server_client: CallingServerClient, call_configuratio async def report(self, incomming_call_context: str): try: - # wait for 10 sec before answering the call. - await asyncio.sleep(10) - # answer call response = await self._calling_server_client.answer_call( incomming_call_context, @@ -122,12 +119,7 @@ async def _play_audio_async(self): try: # cancel playing audio await self._cancel_all_media_operations() - self._play_audio_completed_task.set_result(True) - except Exception as ex: - pass - try: - # After playing audio for 10 sec, make toneReceivedCompleteTask true. - self._tone_received_completed_task.set_result(True) + self._play_audio_completed_task.set_result(False) except Exception as ex: pass except Exception as ex: @@ -137,9 +129,7 @@ async def _play_audio_async(self): async def _hang_up_async(self): Logger.log_message(Logger.INFORMATION, "Performing Hangup operation") - hang_up_response = await self._call_connection.hang_up() - Logger.log_message(Logger.INFORMATION, - "hang_up_async response -----> " + hang_up_response) + await self._call_connection.hang_up() async def _cancel_all_media_operations(self): Logger.log_message(Logger.INFORMATION, @@ -201,8 +191,9 @@ def play_prompt_response_notification(call_event): def _register_to_dtmf_result_event(self, call_leg_id): self._tone_received_completed_task = asyncio.Future() + loop = asyncio.get_event_loop() - async def dtmf_received_event(call_event): + def dtmf_received_event(call_event): tone_received_event: ToneReceivedEvent = call_event tone_info: ToneInfo = tone_received_event.tone_info @@ -219,10 +210,19 @@ async def dtmf_received_event(call_event): self._tone_received_completed_task.set_result(False) except: pass + EventDispatcher.get_instance().unsubscribe( CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id) # cancel playing audio - await self._cancel_all_media_operations() + + # asyncio.run(self._cancel_all_media_operations()) + 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( From 15077ceaf15412c683752c3c437bdef9ea2806d5 Mon Sep 17 00:00:00 2001 From: James Deng Date: Fri, 14 Jan 2022 13:49:20 -0800 Subject: [PATCH 15/15] remove comment out code --- IncomingCallRouting/Utils/IncomingCallHandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IncomingCallRouting/Utils/IncomingCallHandler.py b/IncomingCallRouting/Utils/IncomingCallHandler.py index 9e32add..6907963 100644 --- a/IncomingCallRouting/Utils/IncomingCallHandler.py +++ b/IncomingCallRouting/Utils/IncomingCallHandler.py @@ -213,9 +213,9 @@ def dtmf_received_event(call_event): EventDispatcher.get_instance().unsubscribe( CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id) - # cancel playing audio - # asyncio.run(self._cancel_all_media_operations()) + + # cancel playing audio future = asyncio.run_coroutine_threadsafe(self._cancel_all_media_operations(), loop) future.result()