AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Amazon Connect chat integration.

Globals:
  Function:
    Timeout: 60
    MemorySize: 256
    Runtime: python3.8
  Api:
    Cors:
      AllowMethods: "'*'"
      AllowHeaders: "'*'"
      AllowOrigin: "'*'"

Resources:

  chatDependencies:
    Type: AWS::Serverless::LayerVersion
    Properties:
      ContentUri: dependencies/
      CompatibleRuntimes:
        - python3.10
        - python3.9
        - python3.8
    Metadata:
      BuildMethod: python3.8
      
  ActiveConnections:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        -
          AttributeName: "contactId"
          AttributeType: "S"
        -
          AttributeName: "custID"
          AttributeType: "S"
      KeySchema:
        -
          AttributeName: "contactId"
          KeyType: "HASH"
          
      BillingMode: "PAY_PER_REQUEST"
      
      GlobalSecondaryIndexes:
        -
          IndexName: "custID-index"
          KeySchema:
            -
              AttributeName: "custID"
              KeyType: "HASH"
          Projection:
            ProjectionType: "ALL"

  dialSFRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        -
          PolicyName: LogAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogDelivery
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:GetLogEvents
                Resource:
                  - '*'
        -
          PolicyName: InvokeLambdaFunctions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: Allow
                Action: lambda:InvokeFunction
                Resource:
                  - !GetAtt dial.Arn
                  - !GetAtt getAvailableAgents.Arn

  ConnectChatLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/service-role/AWSLambdaKinesisExecutionRole
      Policies:
        -
          PolicyName: ConnectStartContact
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: Allow
                Action:
                  - connect:UpdateContactAttributes
                  - connect:StartChatContact
                  - connect:StartTaskContact
                  - connect:StartContactStreaming
                  - connect:UpdateContact
                  - connect:StopContact
                Resource:
                  - '*'
        -
          PolicyName: ConnectionsTableAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:DeleteItem
                  - dynamodb:GetItem
                  - dynamodb:Scan
                  - dynamodb:Query
                  - dynamodb:UpdateItem
                Resource:
                  - !GetAtt ActiveConnections.Arn
                  - Fn::Join: [ '/', [ !GetAtt ActiveConnections.Arn, 'index/custID-index' ] ] 
        -
          PolicyName: TopicAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: Allow
                Action:
                  - sns:Publish
                  - sns:Subscribe
                Resource:
                  - !Ref messageExchange
        -
          PolicyName: ConfigurationAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: Allow
                Action:
                  - secretsManager:GetSecretValue
                Resource:
                  - !Ref ConnectChatConfig
        -
          PolicyName: ControlStateMachine
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: Allow
                Action:
                  - states:DescribeExecution
                  - states:StartExecution
                  - states:StopExecution
                Resource: 
                  - '*'
        -
          PolicyName: s3Access
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:ListBucket
                Resource: 
                  - '*'
  encryptionkey:
    Type: AWS::KMS::Key
    Properties: 
      Description: Connect Messages Encryption Key
      Enabled: True
      EnableKeyRotation: True
      KeySpec: SYMMETRIC_DEFAULT
      KeyPolicy:
        Version: 2012-10-17
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal: 
              AWS:
                Fn::Join: 
                - ""
                - 
                  - "arn:aws:iam::"
                  - Ref: "AWS::AccountId"
                  - ":root"
            Action: 'kms:*'
            Resource: '*'
          - Sid: "Enable Services Access"
            Effect: "Allow"
            Principal: 
              Service:
                - 'connect.amazonaws.com'
            Action:
              - 'kms:GenerateDataKey*'
              - 'kms:Decrypt'
            Resource: '*'


  messageAPI:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: !Join 
        - ''
        - - !Ref 'AWS::StackName'
          - '-messageAPI'
      Description: API used for sending messages from external services.
      FailOnWarnings: true

  TwilioLambdaPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:invokeFunction'
      FunctionName: !GetAtt 
        - twilioIncomingMessage
        - Arn
      Principal: apigateway.amazonaws.com
      SourceArn: !Join 
        - ''
        - - 'arn:aws:execute-api:'
          - !Ref 'AWS::Region'
          - ':'
          - !Ref 'AWS::AccountId'
          - ':'
          - !Ref messageAPI
          - /*

  HealthcheckLambdaPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:invokeFunction'
      FunctionName: !GetAtt 
        - healthcheck
        - Arn
      Principal: apigateway.amazonaws.com
      SourceArn: !Join 
        - ''
        - - 'arn:aws:execute-api:'
          - !Ref 'AWS::Region'
          - ':'
          - !Ref 'AWS::AccountId'
          - ':'
          - !Ref messageAPI
          - /*
  CloudAPILambdaPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:invokeFunction'
      FunctionName: !GetAtt 
        - cloudAPIIncomingMessage
        - Arn
      Principal: apigateway.amazonaws.com
      SourceArn: !Join 
        - ''
        - - 'arn:aws:execute-api:'
          - !Ref 'AWS::Region'
          - ':'
          - !Ref 'AWS::AccountId'
          - ':'
          - !Ref messageAPI
          - /*
  ApiGatewayCloudWatchLogsRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - apigateway.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: ApiGatewayLogsPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:DescribeLogGroups'
                  - 'logs:DescribeLogStreams'
                  - 'logs:PutLogEvents'
                  - 'logs:GetLogEvents'
                  - 'logs:FilterLogEvents'
                Resource: '*'
  ApiGatewayAccount:
    Type: 'AWS::ApiGateway::Account'
    Properties:
      CloudWatchRoleArn: !GetAtt 
        - ApiGatewayCloudWatchLogsRole
        - Arn
        
  MessageApiStage:
    DependsOn:
      - ApiGatewayAccount
      - TwilioMessageRequest
      - TwilioCallbackRequest
      - WhatsCloudAPIMessage
      - WhatsCloudAPIHealthcheck
    Type: 'AWS::ApiGateway::Stage'
    Properties:
      DeploymentId: !Ref ApiDeployment
      MethodSettings:
        - DataTraceEnabled: true
          HttpMethod: '*'
          LoggingLevel: INFO
          ResourcePath: /*
      RestApiId: !Ref messageAPI
      StageName: Prod
  
  ApiDeployment:
    Type: 'AWS::ApiGateway::Deployment'
    DependsOn:
      - TwilioMessageRequest
      - TwilioCallbackRequest
      - WhatsCloudAPIMessage
      - WhatsCloudAPIHealthcheck
    Properties:
      RestApiId: !Ref messageAPI
      StageName: Dev

  TwilioMessageResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref messageAPI
      ParentId: !GetAtt 
        - messageAPI
        - RootResourceId
      PathPart: twilio
  TwilioMessageRequest:
    DependsOn: TwilioLambdaPermission
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      HttpMethod: POST
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Uri: !Join 
          - ''
          - - 'arn:aws:apigateway:'
            - !Ref 'AWS::Region'
            - ':lambda:path/2015-03-31/functions/'
            - !GetAtt 
              - twilioIncomingMessage
              - Arn
            - /invocations
        IntegrationResponses:
          - StatusCode: 204
        PassthroughBehavior: 'WHEN_NO_TEMPLATES'
        RequestTemplates:
          application/json: !Join 
            - ''
            - - '{'
              - '  "name": "$input.params(''name'')"'
              - '}'
          application/x-www-form-urlencoded: !Join 
            - ""
            - - "{\n"
              - "#foreach( $token in $input.path('$').split('&') )\n"
              - "   #set( $keyVal = $token.split('=') )\n"
              - "   #set( $keyValSize = $keyVal.size() )\n"
              - "   #if( $keyValSize >= 1 )\n"
              - "       #set( $key = $util.urlDecode($keyVal[0]) )\n"
              - "       #if( $keyValSize >= 2 )\n"
              - "           #set( $val = $util.urlDecode($keyVal[1]) )\n"
              - "       #else\n"
              - "           #set( $val = '' )\n"
              - "       #end\n"
              - "       \"$key\": \"$util.escapeJavaScript($val)\"#if($foreach.hasNext),#end\n"
              - "       #end\n"
              - "#end\n"
              - "}"

      RequestParameters:
        method.request.querystring.name: false
      ResourceId: !Ref TwilioMessageResource
      RestApiId: !Ref messageAPI
      MethodResponses:
        - StatusCode: 204
        
  TwilioCallbackResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref messageAPI
      ParentId: !Ref TwilioMessageResource
      PathPart: callback
      
  TwilioCallbackRequest:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      HttpMethod: POST
      Integration:
        Type: MOCK
        IntegrationResponses:
          - StatusCode: 204
        PassthroughBehavior: 'WHEN_NO_TEMPLATES'
        RequestTemplates:
          application/json: !Join 
            - ''
            - - '{'
              - '"statusCode": 204'
              - '}'
          application/x-www-form-urlencoded: !Join
            - ''
            - - '{'
              - '"statusCode": 204'
              - '}'          

      ResourceId: !Ref TwilioCallbackResource
      RestApiId: !Ref messageAPI
      MethodResponses:
        - StatusCode: 204

  WhatsCloudAPIResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref messageAPI
      ParentId: !GetAtt 
        - messageAPI
        - RootResourceId
      PathPart: cloudapi
      
  WhatsCloudAPIHealthcheck:
    DependsOn: HealthcheckLambdaPermission
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      HttpMethod: GET
      MethodResponses:
        - StatusCode: 200
          ResponseModels:
            text/html: Empty
          ResponseParameters:
            method.response.header.Content-Type: true
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Uri: !Join 
          - ''
          - - 'arn:aws:apigateway:'
            - !Ref 'AWS::Region'
            - ':lambda:path/2015-03-31/functions/'
            - !GetAtt 
              - healthcheck
              - Arn
            - /invocations
        IntegrationResponses:
          - StatusCode: 200
            ResponseParameters:
              method.response.header.Content-Type: "'text/html'"
            ResponseTemplates:
              text/html: !Join
                - ""
                - - "$input.path('$')"
                  - ""
        PassthroughBehavior: 'WHEN_NO_TEMPLATES'
        RequestTemplates:
          application/json: !Join 
            - ""
            - - "#set($allParams = $input.params())\n"
              - "{\n"
              - "\"params\" : { \n"
              - "#foreach($type in $allParams.keySet())\n"
              - "    #set($params = $allParams.get($type))\n"
              - "\"$type\" : { \n"
              - "   #foreach($paramName in $params.keySet())\n"
              - "\"$paramName\" : \"$util.escapeJavaScript($params.get($paramName))\"\n"
              - "      #if($foreach.hasNext),#end\n"
              - "   #end\n"
              - "}\n"
              - "      #if($foreach.hasNext),#end\n"
              - "   #end\n"
              - "}\n"
              - "}"

      RequestParameters:
        method.request.querystring.name: false
      ResourceId: !Ref WhatsCloudAPIResource
      RestApiId: !Ref messageAPI

  WhatsCloudAPIMessage:
    DependsOn: CloudAPILambdaPermission
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      HttpMethod: POST
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Uri: !Join 
          - ''
          - - 'arn:aws:apigateway:'
            - !Ref 'AWS::Region'
            - ':lambda:path/2015-03-31/functions/'
            - !GetAtt 
              - cloudAPIIncomingMessage
              - Arn
            - /invocations
        IntegrationResponses:
          - StatusCode: 200
            ResponseParameters:
              method.response.header.Content-Type: "'text/html'"
        PassthroughBehavior: 'WHEN_NO_TEMPLATES'
        RequestTemplates:
          application/json: !Join 
            - ''
            - - "{\n"
              - "  \"body-json\": $input.json('$')\n"
              - "}\n"
      RequestParameters:
        method.request.querystring.name: false
      ResourceId: !Ref WhatsCloudAPIResource
      RestApiId: !Ref messageAPI
      MethodResponses:
        - StatusCode: 200
          ResponseModels:
            text/html: Empty
          ResponseParameters:
            method.response.header.Content-Type: true
            
  messageExchange:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: 'Amazon Connect Chat Message Exchange'
      KmsMasterKeyId: !Ref encryptionkey

  ConnectChatConfig:
    Type: AWS::SecretsManager::Secret
    Properties:
      SecretString: '{"CONNECT_INSTANCE_ID":"Replace with instance ID data","CONNECT_QUEUE_ID":"Replace with queue ID data","CONTACT_FLOW_ID":"Replace with contact flow ID data","TWILIO_SID":"Replace with account SID","TWILIO_AUTH_TOKEN":"Replace with authentication token","TWILIO_FROM_NUMBER":"Replace with phone -FROM- number","WHATS_TOKEN":"Replace with whatsApp phone Token","WHATS_PHONE_ID":"Replace with whatsApp phone ID","WHATS_VERIFICATION_TOKEN":"Replace with WhatsApp verification Token"}'
      Tags:
        -
          Key: AppName
          Value: ConnectChat


  twilioIncomingMessage:
    Type: AWS::Serverless::Function 
    Properties:
      Role: !GetAtt ConnectChatLambdaRole.Arn
      CodeUri: twilio-processExternal/
      Handler: lambda_function.lambda_handler
      Environment:
        Variables:
          CONFIG_PARAMETER: !Ref ConnectChatConfig
          ACTIVE_CONNNECTIONS: !Ref ActiveConnections
          SNS_TOPIC: !Ref messageExchange
      Layers:
        - !Ref chatDependencies
        
  cloudAPIIncomingMessage:
    Type: AWS::Serverless::Function 
    Properties:
      Role: !GetAtt ConnectChatLambdaRole.Arn
      CodeUri: cloudapi-processExternal/
      Handler: lambda_function.lambda_handler
      Environment:
        Variables:
          CONFIG_PARAMETER: !Ref ConnectChatConfig
          ACTIVE_CONNNECTIONS: !Ref ActiveConnections
          SNS_TOPIC: !Ref messageExchange
      Layers:
        - !Ref chatDependencies

  healthcheck:
    Type: AWS::Serverless::Function 
    Properties:
      Role: !GetAtt ConnectChatLambdaRole.Arn
      CodeUri: healthcheck/
      Handler: lambda_function.lambda_handler
      Environment:
        Variables:
          CONFIG_PARAMETER: !Ref ConnectChatConfig
      Layers:
        - !Ref chatDependencies

  callbackDial:
    Type: AWS::Serverless::StateMachine
    Properties:
      DefinitionUri: statemachine/dial.asl.json
      Role: !GetAtt dialSFRole.Arn
      DefinitionSubstitutions:
        dial: !GetAtt dial.Arn
        getAvailableAgents: !GetAtt getAvailableAgents.Arn
  manualCallback:
    Type: AWS::Serverless::Function 
    Properties:
      Role: !GetAtt ConnectChatLambdaRole.Arn
      CodeUri: manualcallback/
      Handler: lambda_function.lambda_handler
      Environment: 
        Variables:
          MACHINE_ID: !Ref callbackDial
          
  dial:
    Type: AWS::Serverless::Function 
    Properties:
      Role: !GetAtt ConnectChatLambdaRole.Arn
      CodeUri: callback-dial/
      Handler: lambda_function.lambda_handler
      Environment: 
        Variables:
          CONFIG_SECRET: !Ref ConnectChatConfig
  getAvailableAgents:
    Type: AWS::Serverless::Function 
    Properties:
      Role: !GetAtt ConnectChatLambdaRole.Arn
      CodeUri: getAvailableAgents/
      Handler: lambda_function.lambda_handler
      Environment: 
        Variables:
          CONFIG_SECRET: !Ref ConnectChatConfig

  processConnectMessage:
    Type: AWS::Serverless::Function 
    Properties:
      Role: !GetAtt ConnectChatLambdaRole.Arn
      CodeUri: processConnectMessage/
      Handler: lambda_function.lambda_handler
      Environment: 
        Variables:
          CONFIG_PARAMETER: !Ref ConnectChatConfig
          ACTIVE_CONNNECTIONS: !Ref ActiveConnections
      Layers:
        - !Ref chatDependencies

      Events:
        incomingmessageSNS:
          Type: SNS
          Properties:
            Topic: !Ref messageExchange