import os import decimal import json import logging import boto3 import botocore.exceptions chime_sdk_meeting_client = boto3.client('chime-sdk-meetings') dynamo_client = boto3.resource('dynamodb') MEETING_TABLE = os.environ['MEETING_TABLE'] meeting_table = dynamo_client.Table(MEETING_TABLE) # Set LOG_LEVEL using environment variable, fallback to INFO if not present logger = logging.getLogger() try: LOG_LEVEL = os.environ['LOG_LEVEL'] if LOG_LEVEL not in ['INFO', 'DEBUG', 'WARN', 'ERROR']: LOG_LEVEL = 'INFO' except BaseException: LOG_LEVEL = 'INFO' logger.setLevel(LOG_LEVEL) class DecimalEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, decimal.Decimal): return int(obj) return super(DecimalEncoder, self).default(obj) def handler(event, context): event_type = event['InvocationEventType'] transaction_id = event['CallDetails']['TransactionId'] transaction_attributes = event['CallDetails'].get('TransactionAttributes') if transaction_attributes is None: transaction_attributes = {} participants = event['CallDetails']['Participants'] call_id = participants[0]['CallId'] global LOG_PREFIX LOG_PREFIX = f'SMA Handler: ' logger.info('%s RECV Event: %s', LOG_PREFIX, json.dumps(event, indent=4)) if event_type == 'NEW_INBOUND_CALL': transaction_attributes['call_type'] = 'inbound' return response(inbound_call_speak_and_get_digits_action("<speak>Please enter your 6 digit event i d</speak>"), transaction_attributes=transaction_attributes) elif event_type == 'HANGUP': if participants[0]['To'] == '+17035550122': return response(hangup_action(participants[1]['CallId']), transaction_attributes=transaction_attributes) elif len(participants) == 2: logger.info('%s Deleting attendee %s in meeting %s', LOG_PREFIX, transaction_attributes['attendee_id'], transaction_attributes['meeting_id']) chime_sdk_meeting_client.delete_attendee(MeetingId=transaction_attributes['meeting_id'], AttendeeId=transaction_attributes['attendee_id']) current_attendee_list = chime_sdk_meeting_client.list_attendees(MeetingId=transaction_attributes['meeting_id']) logger.info('Current Attendee List: %s', json.dumps(current_attendee_list['Attendees'])) if len(current_attendee_list['Attendees']) == 0: logger.info('%s No more attendees, deleting meeting: %s', LOG_PREFIX, transaction_attributes['meeting_id']) chime_sdk_meeting_client.delete_meeting(MeetingId=transaction_attributes['meeting_id']) return response(transaction_attributes=transaction_attributes) else: return response(hangup_action(call_id), transaction_attributes=transaction_attributes) elif event_type == 'NEW_OUTBOUND_CALL': logger.info('%s Adding transaction attributes', LOG_PREFIX) transaction_attributes['meeting_id'] = event['ActionData']['Parameters']['Arguments']['meeting_id'] transaction_attributes['attendee_id'] = event['ActionData']['Parameters']['Arguments']['attendee_id'] transaction_attributes['join_token'] = event['ActionData']['Parameters']['Arguments']['join_token'] transaction_attributes['event_id'] = event['ActionData']['Parameters']['Arguments']['event_id'] transaction_attributes['meeting_passcode'] = event['ActionData']['Parameters']['Arguments']['meeting_passcode'] transaction_attributes['phone_number'] = event['ActionData']['Parameters']['Arguments']['phone_number'] transaction_attributes['call_type'] = 'outbound' return response(transaction_attributes=transaction_attributes) elif event_type == 'CALL_ANSWERED': return response(outbound_call_speak_and_get_digits_action(transaction_attributes), transaction_attributes=transaction_attributes) elif event_type == 'ACTION_SUCCESSFUL': logger.info('%s Action Successful', LOG_PREFIX) if event['ActionData']['Type'] == 'SpeakAndGetDigits': logger.info('%s SpeakAndGetDigits Action Successful', LOG_PREFIX) if transaction_attributes['call_type'] == 'outbound': logger.info('%s CallType is outbound', LOG_PREFIX) received_digits = event['ActionData']['ReceivedDigits'] if received_digits == '1': logger.info('%s Received digits is 1', LOG_PREFIX) update_table(transaction_attributes, transaction_attributes['meeting_id'], transaction_attributes['attendee_id']) return response(join_chime_meeting_action(call_id, transaction_attributes), transaction_attributes=transaction_attributes) else: logger.info('%s Received digits is not 1', LOG_PREFIX) return response(speak_action(call_id, "Disconnecting you."), hangup_action(call_id), transaction_attributes=transaction_attributes) elif transaction_attributes['call_type'] == 'inbound': logger.info('%s CallType is inbound', LOG_PREFIX) received_digits = event['ActionData']['ReceivedDigits'] if 'event_id' not in transaction_attributes: logger.info('%s Event Id not in transaction attributes', LOG_PREFIX) transaction_attributes['event_id'] = received_digits return response( inbound_call_speak_and_get_digits_action("<speak>Please enter your 6 digit passcode to join the meeting.</speak>"), transaction_attributes=transaction_attributes) else: logger.info('%s Event ID is in transaction attributes', LOG_PREFIX) try: logger.info('%s Getting Item from DynamoDB for Event ID: %s and Passcode: %s', LOG_PREFIX, transaction_attributes['event_id'], received_digits) event_info = meeting_table.get_item(Key={"EventId": transaction_attributes['event_id'], 'MeetingPasscode': received_digits}) logger.info('%s Event Info: %s', LOG_PREFIX, json.dumps(event_info, cls=DecimalEncoder, indent=4)) except Exception as error: logger.error('%s DynamoDB Exception: %s', LOG_PREFIX, error) raise error if event_info.get('Item'): logger.info('%s Passcode and Event ID combination is valid', LOG_PREFIX) transaction_attributes['phone_number'] = event_info['Item']['PhoneNumber'] transaction_attributes['event_id'] = str(event_info['Item']['EventId']) transaction_attributes['meeting_passcode'] = received_digits transaction_attributes['meeting_id'] = event_info['Item']['MeetingId'] meeting_info = create_meeting(transaction_attributes) # transaction_attributes['meeting_id'] = meeting_info['Meeting']['MeetingId'] transaction_attributes['attendee_id'] = meeting_info['Attendees'][0]['AttendeeId'] transaction_attributes['join_token'] = meeting_info['Attendees'][0]['JoinToken'] return response(join_chime_meeting_action(call_id, transaction_attributes), transaction_attributes=transaction_attributes) else: logger.info('%s Passcode and Event ID combination is not valid', LOG_PREFIX) return response(speak_action(call_id, "Invalid meeting passcode."), hangup_action(call_id), transaction_attributes=transaction_attributes) if event['ActionData']['Type'] == 'JoinChimeMeeting': logger.info('%s JoinChimeMeetingAction Successful', LOG_PREFIX) return response(speak_action(call_id, "You have been joined to the meeting."), transaction_attributes=transaction_attributes) else: logger.info('%s Action Type is not SpeakAndGetDigits or JoinChimeMeeting', LOG_PREFIX) return response(transaction_attributes=transaction_attributes) elif event_type == 'ACTION_FAILED': logger.info('%s Action Failed', LOG_PREFIX) if event['ActionData']['Type'] == 'JoinChimeMeeting': logger.info('%s JoinChimeMeetingAction Failed', LOG_PREFIX) return response(speak_action(call_id, "Sorry, I could not connect you to the meeting"), hangup_action(call_id), transaction_attributes=transaction_attributes) if event['ActionData']['Type'] == 'SpeakAndGetDigits': logger.info('%s SpeakAndGetDigits Failed', LOG_PREFIX) if event['ActionData']['ErrorType'] == 'InvalidDigitsReceived': logger.info('%s InvalidDigitsReceived', LOG_PREFIX) return response(hangup_action(call_id), transaction_attributes=transaction_attributes) else: return response(transaction_attributes=transaction_attributes) else: return response(transaction_attributes=transaction_attributes) def response(*actions, transaction_attributes): res = { 'SchemaVersion': '1.0', 'Actions': [*actions], 'TransactionAttributes': transaction_attributes } logger.info('%s RESPONSE %s', LOG_PREFIX, json.dumps(res, indent=4)) return res def outbound_call_speak_and_get_digits_action(transaction_attributes): return { "Type": "SpeakAndGetDigits", "Parameters": { "MinNumberOfDigits": 1, "MaxNumberOfDigits": 1, "Repeat": 3, "RepeatDurationInMilliseconds": 3000, "InputDigitsRegex": "[1-2]", "InBetweenDigitsDurationInMilliseconds": 1000, "TerminatorDigits": ["#"], "SpeechParameters": { "Text": "<speak>You are needed on a call for event <say-as interpret-as='digits'>" + transaction_attributes['event_id'] + "</say-as>. Press 1 to join, 2 to decline.</speak>", "Engine": "neural", "LanguageCode": "en-US", "TextType": "ssml", "VoiceId": "Joanna"}, "FailureSpeechParameters": { "Text": "Sorry, I didn't get that. Please press 1 to join, 2 to decline.", "Engine": "neural", "LanguageCode": "en-US", "TextType": "text", "VoiceId": "Joanna"}, }, } def inbound_call_speak_and_get_digits_action(text): return { "Type": "SpeakAndGetDigits", "Parameters": { "MinNumberOfDigits": 6, "MaxNumberOfDigits": 6, "Repeat": 3, "RepeatDurationInMilliseconds": 7500, "InputDigitsRegex": "[0-9]", "InBetweenDigitsDurationInMilliseconds": 1000, "TerminatorDigits": ["#"], "SpeechParameters": { "Text": text, "Engine": "neural", "LanguageCode": "en-US", "TextType": "ssml", "VoiceId": "Joanna"}, "FailureSpeechParameters": { "Text": "Sorry, I didn't get that. Please try again.", "Engine": "neural", "LanguageCode": "en-US", "TextType": "text", "VoiceId": "Joanna"}, }, } def speak_action(call_id, message): return { "Type": "Speak", "Parameters": { "Text": message, "CallId": call_id, "Engine": "neural", "LanguageCode": "en-US", "TextType": "text", "VoiceId": "Joanna" } } def hangup_action(call_id): return { 'Type': 'Hangup', 'Parameters': { "CallId": call_id, 'SipResponseCode': '0' } } def join_chime_meeting_action(call_id, transaction_attributes): return { 'Type': 'JoinChimeMeeting', 'Parameters': { "JoinToken": transaction_attributes['join_token'], "CallId": call_id, "MeetingId": transaction_attributes['meeting_id'] } } def create_meeting(transaction_attributes): logger.info('%s Creating meeting for event %s', LOG_PREFIX, transaction_attributes['event_id']) check_attendee(transaction_attributes) try: meeting_info = chime_sdk_meeting_client.create_meeting_with_attendees( ClientRequestToken=transaction_attributes['event_id'], MediaRegion='us-east-1', ExternalMeetingId=transaction_attributes['event_id'], Attendees=[{ 'ExternalUserId': transaction_attributes['phone_number'] }] ) update_table(transaction_attributes, meeting_info['Meeting']['MeetingId'], meeting_info['Attendees'][0]['AttendeeId']) logger.info('%s Meeting created: %s', LOG_PREFIX, meeting_info) return meeting_info except Exception as error: logger.error('%s Error creating meeting: %s', LOG_PREFIX, error) return error def check_attendee(transaction_attributes): logger.info('%s Getting attendee list for meeting %s', LOG_PREFIX, transaction_attributes['meeting_id']) try: attendee_info = chime_sdk_meeting_client.list_attendees( MeetingId=transaction_attributes['meeting_id'], ) for attendee in attendee_info['Attendees']: if attendee['ExternalUserId'] == transaction_attributes['phone_number']: logger.info('%s Attendee already exists', LOG_PREFIX) delete_attendee(transaction_attributes['meeting_id'], attendee['AttendeeId']) else: continue return True except Exception as error: logger.error('%s Error getting attendee: %s', LOG_PREFIX, error) return error def delete_attendee(meeting_id, attendee_id): logger.info('%s Deleting attendee %s for meeting %s', LOG_PREFIX, attendee_id, meeting_id) try: chime_sdk_meeting_client.delete_attendee( MeetingId=meeting_id, AttendeeId=attendee_id ) logger.info('%s Attendee deleted', LOG_PREFIX) return True except Exception as error: logger.error('%s Error deleting attendee: %s', LOG_PREFIX, error) return error def update_table(transaction_attributes, meeting_id, attendee_id): logger.info('%s Updating table for event %s', LOG_PREFIX, transaction_attributes['event_id']) try: table_update = meeting_table.update_item( Key={"EventId": transaction_attributes['event_id'], "MeetingPasscode": transaction_attributes['meeting_passcode']}, UpdateExpression="set JoinMethod = :j, MeetingId = :m, AttendeeId = :a", ExpressionAttributeValues={":j": 'Phone', ":m": meeting_id, ":a": attendee_id}, ReturnValues="UPDATED_NEW"), logger.info('%s s Table update: %s', LOG_PREFIX, json.dumps(table_update, cls=DecimalEncoder, indent=4)) return True except Exception as error: logger.error('%s Error updating table: %s', LOG_PREFIX, error) return error