Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions IncomingCallRouting/ConfigurationManager.py
Original file line number Diff line number Diff line change
@@ -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
82 changes: 82 additions & 0 deletions IncomingCallRouting/Controllers/IncomingCallController.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import json
import ast
from typing import List
from aiohttp import web
from aiohttp.web_routedef import post
from Utils.Logger import Logger
from Utils.CallConfiguration import CallConfiguration
from azure.communication.callingserver.aio import CallingServerClient
from azure.eventgrid import EventGridEvent
from EventHandler.EventAuthHandler import EventAuthHandler
from EventHandler.EventDispatcher import EventDispatcher
from azure.core.messaging import CloudEvent
from Utils.IncomingCallHandler import IncomingCallHandler


class IncomingCallController:

app = web.Application()

_calling_server_client: CallingServerClient = None
_incoming_calls: List = None
_call_configuration: CallConfiguration = None

def __init__(self, configuration):
self._call_configuration = CallConfiguration.get_call_configuration(
configuration)
self._calling_server_client = CallingServerClient.from_connection_string(
self._call_configuration.connection_string)
self._incoming_calls = []
self.app.add_routes(
[web.post('/OnIncomingCall', self.on_incoming_call)])
self.app.add_routes([web.post(
'/CallingServerAPICallBacks', self.calling_server_api_callbacks)])
web.run_app(self.app, port=9007)


async def on_incoming_call(self, request):
try:
http_content = await request.content.read()
post_data = str(http_content.decode('UTF-8'))
if (post_data):
json_data = ast.literal_eval(json.dumps(post_data))
cloud_event: EventGridEvent = EventGridEvent.from_dict(
ast.literal_eval(json_data)[0])

if(cloud_event.event_type == 'Microsoft.EventGrid.SubscriptionValidationEvent'):
event_data = cloud_event.data
code = event_data['validationCode']

if (code):
response_data = {"ValidationResponse": code}
if(response_data["ValidationResponse"] != None):
return web.Response(body=str(response_data), status=200)
elif (cloud_event.event_type == 'Microsoft.Communication.IncomingCall'):
if(post_data != None and cloud_event.data["to"]['rawId'] == self._call_configuration.bot_identity):
incoming_call_context = post_data.split(
"\"incomingCallContext\":\"")[1].split("\"}")[0]
self._incoming_calls.append(await IncomingCallHandler(self._calling_server_client, self._call_configuration).report(incoming_call_context))

return web.Response(status=200)

except Exception as ex:
raise Exception("Failed to handle incoming call --> " + str(ex))

async def calling_server_api_callbacks(self, request):
try:
event_handler = EventAuthHandler()
param = request.rel_url.query
if (param.get('secret') and event_handler.authorize(param['secret'])):
if (request != None):
http_content = await request.content.read()
Logger.log_message(
Logger.INFORMATION, "CallingServerAPICallBacks -------> " + str(request))
eventDispatcher: EventDispatcher = EventDispatcher.get_instance()
eventDispatcher.process_notification(
str(http_content.decode('UTF-8')))
return web.Response(status=201)
else:
return web.Response(status=401)
except Exception as ex:
raise Exception(
"Failed to handle incoming callbacks --> " + str(ex))
Empty file.
15 changes: 15 additions & 0 deletions IncomingCallRouting/EventHandler/EventAuthHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from Utils.Logger import Logger


class EventAuthHandler:

secret_value = 'h3llowW0rld'

def authorize(self, query):
if query == None:
return False
return ((query != None) and (query == self.secret_value))

def get_secret_querystring(self):
secretKey = "secret"
return (secretKey + "=" + self.secret_value)
104 changes: 104 additions & 0 deletions IncomingCallRouting/EventHandler/EventDispatcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import threading
import json
from Utils.Logger import Logger
from threading import Lock
from azure.core.messaging import CloudEvent
from azure.communication.callingserver import CallingServerEventType, \
CallConnectionStateChangedEvent, ToneReceivedEvent, \
PlayAudioResultEvent, TransferCallResultEvent


class EventDispatcher:
__instance = None
notification_callbacks: dict = None
subscription_lock = None

def __init__(self):
self.notification_callbacks = dict()
self.subscription_lock = Lock()

@staticmethod
def get_instance():
if EventDispatcher.__instance is None:
EventDispatcher.__instance = EventDispatcher()

return EventDispatcher.__instance

def subscribe(self, event_type: str, event_key: str, notification_callback):
self.subscription_lock.acquire
event_id: str = self.build_event_key(event_type, event_key)
self.notification_callbacks[event_id] = notification_callback
self.subscription_lock.release

def unsubscribe(self, event_type: str, event_key: str):
self.subscription_lock.acquire
event_id: str = self.build_event_key(event_type, event_key)
del self.notification_callbacks[event_id]
self.subscription_lock.release

def build_event_key(self, event_type: str, event_key: str):
return event_type + "-" + event_key

def process_notification(self, request: str):
call_event = self.extract_event(request)
if call_event is not None:
self.subscription_lock.acquire
notification_callback = self.notification_callbacks.get(
self.get_event_key(call_event))
if (notification_callback != None):

threading.Thread(target=notification_callback,
args=(call_event,)).start()

def get_event_key(self, call_event_base):
if type(call_event_base) == CallConnectionStateChangedEvent:
call_leg_id = call_event_base.call_connection_id
key = self.build_event_key(
CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT, call_leg_id)
return key
elif type(call_event_base) == ToneReceivedEvent:
call_leg_id = call_event_base.call_connection_id
key = self.build_event_key(
CallingServerEventType.TONE_RECEIVED_EVENT, call_leg_id)
return key
elif type(call_event_base) == PlayAudioResultEvent:
operation_context = call_event_base.operation_context
key = self.build_event_key(
CallingServerEventType.PLAY_AUDIO_RESULT_EVENT, operation_context)
return key
elif type(call_event_base) == TransferCallResultEvent:
call_leg_id = call_event_base.operation_context
key = self.build_event_key(
CallingServerEventType.TRANSFER_CALL_RESULT_EVENT, call_leg_id)
return key
return None

def extract_event(self, request: str):
try:
event = CloudEvent.from_dict(json.loads(request)[0])
print(event)
if event.type == CallingServerEventType.CALL_CONNECTION_STATE_CHANGED_EVENT:
call_connection_state_changed_event = CallConnectionStateChangedEvent.deserialize(
event.data)
return call_connection_state_changed_event

if event.type == CallingServerEventType.PLAY_AUDIO_RESULT_EVENT:
play_audio_result_event = PlayAudioResultEvent.deserialize(
event.data)
return play_audio_result_event

if event.type == CallingServerEventType.TRANSFER_CALL_RESULT_EVENT:
transfer_call_result_event = TransferCallResultEvent.deserialize(
event.data)
return transfer_call_result_event

if event.type == CallingServerEventType.TONE_RECEIVED_EVENT:
tone_received_event = ToneReceivedEvent.deserialize(
event.data)
return tone_received_event

except Exception as ex:
Logger.log_message(
Logger.ERROR, "Failed to parse request content Exception: " + str(ex))

return None
Empty file.
27 changes: 27 additions & 0 deletions IncomingCallRouting/Utils/CallConfiguration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from EventHandler.EventAuthHandler import EventAuthHandler


class CallConfiguration:
callConfiguration = None

def __init__(self, connection_string, app_base_url, audio_file_uri, participant, bot_identity):
self.connection_string: str = str(connection_string)
self.app_base_url: str = str(app_base_url)
self.audio_file_uri: str = str(audio_file_uri)
eventhandler = EventAuthHandler()
self.app_callback_url: str = app_base_url + \
"/CallingServerAPICallBacks?" + eventhandler.get_secret_querystring()
self.target_participant: str = str(participant)
self.bot_identity: str = str(bot_identity)

@classmethod
def get_call_configuration(cls, configuration):
if(cls.callConfiguration == None):
cls.callConfiguration = CallConfiguration(
configuration["connection_string"],
configuration["app_base_url"],
configuration["audio_file_uri"],
configuration["target_participant"],
configuration["bot_identity"])

return cls.callConfiguration
8 changes: 8 additions & 0 deletions IncomingCallRouting/Utils/CommunicationIdentifierKind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import enum


class CommunicationIdentifierKind(enum.Enum):

USER_IDENTITY = "USER_IDENTITY"
PHONE_IDENTITY = "PHONE_IDENTITY"
UNKNOWN_IDENTITY = "UNKNOWN_IDENTITY"
7 changes: 7 additions & 0 deletions IncomingCallRouting/Utils/Constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import enum


class Constants(enum.Enum):

userIdentityRegex = "8:acs:[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}_[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}"
phoneIdentityRegex = "^\+\d{10,14}$"
Loading