# This Python Lambda is an example Chime SipMediaApplcation to demostrate the # CallAndBridge action. Incoming calls are greeted with a welcome message, # before being bridged to a PSTN destination. This sample uses a DynamoDB # table to maintain mappings of dialed numbers to destination PSTN numbers. # The app uses Lambda environment variables and a DynamoDB table that contains # dialed_number & destination_number keys. The Lambda requires a role with # permissions to Get* items from the required DynamoDB table. An environment # variable "LoopGreetingWhileRinging" when set to True will loop the specified # greeting while the Outbound is setup, if set to False will play the greeting # in full before setting up the Outbound call. import os import json import boto3 import logging from botocore.client import Config # Set LogLevel using environment variable, fallback to INFO if not present logger = logging.getLogger() try: log_level = os.environ['LogLevel'] if log_level not in ['INFO', 'DEBUG']: log_level = 'INFO' except: log_level = 'INFO' logger.setLevel(log_level) # Load environment variables forwarding_table_name = os.environ['CallForwardingTableName'] wav_bucket = os.environ['WavBucketName'] loop_flag = os.environ['LoopGreetingWhileRinging'] # Setup DynamoDB interface client to query number mappings client_config = Config(connect_timeout=2, read_timeout=2, retries={'max_attempts': 5}) dynamodb_client = boto3.client('dynamodb', config=client_config, region_name=os.environ["AWS_REGION"]) # This is the entry point for all incoming events from Chime SipMediaApplications def lambda_handler(event, context): # Extract all the elements from the event that we will need for processing event_type = event['InvocationEventType'] participants = event['CallDetails']['Participants'] call_id = participants[0]['CallId'] to_number = participants[0]['To'] from_number = participants[0]['From'] # For consistent and detail logging, set a prefix format that can be used by all functions global log_prefix log_prefix = 'Call-ID:{} {} From:[{}] To:[{}]: '.format(call_id, event_type, from_number, to_number) if event_type == 'NEW_INBOUND_CALL': logger.info('RECV {} {}'.format(log_prefix, 'New inbound call initiated')) return new_call_handler(call_id, to_number) elif event_type == 'HANGUP': logger.info('RECV {} {}'.format(log_prefix, 'Hangup event received')) return hangup_handler(participants) elif event_type == 'RINGING': logger.info('RECV {} {}'.format(log_prefix, 'Ringing event received')) return () elif event_type == 'ACTION_SUCCESSFUL': return action_success_handler(event) elif event_type == 'ACTION_FAILED': logger.error('RECV {} {} {} {}'.format(log_prefix, event['ActionData']['ErrorType'], event['ActionData']['ErrorMessage'], json.dumps(event))) return unable_to_connect(call_id) elif event_type == 'INVALID_LAMBDA_RESPONSE': logger.error('RECV {} : {} : {} : {}'.format(log_prefix, event['ErrorType'], event['ErrorMessage'], json.dumps(event))) return unable_to_connect(call_id) else: logger.error('RECV {} [Unhandled event] {}'.format(log_prefix, json.dumps(event))) return unable_to_connect(call_id) def new_call_handler(call_id, dialed_number): destination_number = ddb_get_destination(dialed_number) if destination_number: if loop_flag == 'True': # Loop Audio Greeting until Outbound call answers, cut greeting audio as soon as Outbound call answers logger.info('SEND {} {}'.format(log_prefix, 'Sending CallAndBridge action with greeting ringback')) return respond(call_and_bridge_to_pstn_with_greeting(dialed_number, destination_number, 'please_wait_while_we_try_to_connect_you.wav')) else: # Play Audio Greeting fully and then bridge to Outbound PSTN logger.info('SEND {} {}'.format(log_prefix, 'Sending PlayAudio, CallAndBridge actions')) return respond(play_audio(call_id, 'please_wait_while_we_try_to_connect_you.wav'), call_and_bridge_to_pstn(dialed_number, destination_number)) else: logger.info('NONE {} {}'.format(log_prefix, 'No entry found in database - sending hangup')) return unable_to_connect(call_id) def hangup_handler(participants): # When we receive a hangup event, we make sure to tear down any calls still connected for call in participants: if call['Status'] == 'Connected': return respond(hangup_action(call['CallId'])) logger.info('NONE {} All calls have been hungup'.format(log_prefix)) return respond() # A DDB table keeps a mapping of dialed number (called/to number) to destination number (number to be bridged to). # We query the destination number using the dialed number. def ddb_get_destination(to_number): try: response = dynamodb_client.get_item( Key={ 'dialed_number': { 'S': str(to_number), }, }, TableName=forwarding_table_name, ) if 'Item' in response: return response['Item']['destination_number']['S'] except Exception as err: logger.error('DynamoDB Query error: failed to fetch data from table. Error: ', exc_info=err) return None # If we receive an ACTION_SUCCESSFUL event we can take further actions, # or default to responding with a NoOp (empty set of actions) def action_success_handler(event): action = event['ActionData']['Type'] if action == 'Answer': return respond() elif action == 'Hangup': return respond() elif action == 'CallAndBridge': logger.info('RECV {} Connected to Outbound call'.format(log_prefix)) return respond() return respond() # A wrapper for all responses back to the service def respond(*actions): return { 'SchemaVersion': '1.0', 'Actions': [*actions] } # SipResponseCode can be parameterized. Supported values: '480' - Unavailable, '486' - Busy, '0' - Terminated # To read more on customizing the hangup action, see https://docs.aws.amazon.com/chime/latest/dg/hangup.html def hangup_action(call_id): logger.info('SEND {} {} {}'.format(log_prefix, 'Sending HANGUP action to Call-ID', call_id)) return { 'Type': 'Hangup', 'Parameters': { 'CallId': call_id, 'SipResponseCode': '0' } } # Used for playing audio greetings to callers - files should be stored in S3, with the bucket name as a Lambda environment variable def play_audio(call_id, audio_file): return { 'Type': 'PlayAudio', 'Parameters': { 'CallId': call_id, 'AudioSource': { 'Type': 'S3', 'BucketName': wav_bucket, 'Key': audio_file } } } def call_and_bridge_to_pstn(caller_id, destination): return { 'Type': 'CallAndBridge', 'Parameters': { 'CallTimeoutSeconds': 30, 'CallerIdNumber': caller_id, 'Endpoints': [ { 'Uri': destination, 'BridgeEndpointType': 'PSTN' } ] } } def call_and_bridge_to_pstn_with_greeting(caller_id, destination, audio_file): return { 'Type': 'CallAndBridge', 'Parameters': { 'CallTimeoutSeconds': 30, 'CallerIdNumber': caller_id, 'RingbackTone': { 'Type': 'S3', 'BucketName': os.environ['WavBucketName'], 'Key': audio_file }, 'Endpoints': [ { 'Uri': destination, 'BridgeEndpointType': 'PSTN' } ] } } # A predefined set of actions that plays an error to the caller and then hangs up def unable_to_connect(call_id): return respond(play_audio(call_id, 'we_were_unable_to_connect_your_call.wav'), hangup_action(call_id))