Repository: Miserlou/Zappa

Issue:
support AWS Lex Bot events
## Context
Currently zappa supports excuting django [AWS events](https://github.com/Miserlou/Zappa#executing-in-response-to-aws-events). It doesn't recognise the Lex bot event. It explicitly checks for `'Records'` inside the request. I want to organise all Lex bot functions inside the django app itself. So that I will have access to database and others. [Here](https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-lex) is a sample Lex event source format

## Possible Fix
add new section to `events ` settings where we can configure the `intent's hook`.
```js
"events": [
      {
        // The function to execute
        "function": "mailer.tasks.send_emails",
        // When to execute it (in cron or rate format)
        "expression": "rate(5 minutes)"
      },
      {
        "function": "lexbot.handlers.book_appointment.handler",
        "event_source": {
          // intent's ARN : arn:aws:lex:region:accountId:
          "arn": "arn:aws:lex:<region>:<account-id>:intent:<intent-name>:$LATEST",
          // a list of configured invocations. possible values are from [FulfillmentCodeHook , DialogCodeHook]
          "events": [
            "DialogCodeHook"
          ]
        }
      }
    ],
  
```
 I will create an MR with support for this. It would not require much changes since there are similar invocation code inside handler. But we should arrive at a definitive way to get the function configuration from zapp_settings.json

## Your Environment
* Zappa version used: 0.45.1
* Operating System and Python version: Python 3.6



Discussion:

Resolving patch:
diff --git a/zappa/cli.py b/zappa/cli.py
--- a/zappa/cli.py
+++ b/zappa/cli.py
@@ -2297,6 +2297,19 @@ def create_package(self, output=None):
                     event_mapping[arn] = function
             settings_s = settings_s + "AWS_EVENT_MAPPING={0!s}\n".format(event_mapping)
 
+            # Map Lext bot events
+            bot_events = self.stage_config.get('bot_events', [])
+            bot_events_mapping = {}
+            for bot_event in bot_events:
+                event_source = bot_event.get('event_source', {})
+                intent = event_source.get('intent')
+                invocation_source = event_source.get('invocation_source')
+                function = bot_event.get('function')
+                if intent and invocation_source and function:
+                    bot_events_mapping[str(intent) + ':' + str(invocation_source)] = function
+
+            settings_s = settings_s + "AWS_BOT_EVENT_MAPPING={0!s}\n".format(bot_events_mapping)
+
             # Map cognito triggers
             cognito_trigger_mapping = {}
             cognito_config = self.stage_config.get('cognito', {})
diff --git a/zappa/handler.py b/zappa/handler.py
--- a/zappa/handler.py
+++ b/zappa/handler.py
@@ -303,6 +303,18 @@ def get_function_for_aws_event(self, record):
 
         return None
 
+    def get_function_from_bot_intent_trigger(self, event):
+        """
+        For the given event build ARN and return the configured function
+        """
+        intent = event.get('currentIntent')
+        if intent:
+            intent = intent.get('name')
+            if intent:
+                return self.settings.AWS_BOT_EVENT_MAPPING.get(
+                    "{}:{}".format(intent, event.get('invocationSource'))
+                )
+
     def get_function_for_cognito_trigger(self, trigger):
         """
         Get the associated function to execute for a cognito trigger
@@ -397,6 +409,18 @@ def handler(self, event, context):
                 logger.error("Cannot find a function to process the triggered event.")
             return result
 
+        # this is an AWS-event triggered from Lex bot's intent
+        elif event.get('bot'):
+            result = None
+            whole_function = self.get_function_from_bot_intent_trigger(event)
+            if whole_function:
+                app_function = self.import_module_and_get_function(whole_function)
+                result = self.run_function(app_function, event, context)
+                logger.debug(result)
+            else:
+                logger.error("Cannot find a function to process the triggered event.")
+            return result
+
         # This is an API Gateway authorizer event
         elif event.get('type') == u'TOKEN':
             whole_function = self.settings.AUTHORIZER_FUNCTION


Test patch:
diff --git a/tests/test_bot_handler_being_triggered.py b/tests/test_bot_handler_being_triggered.py
new file mode 100644
--- /dev/null
+++ b/tests/test_bot_handler_being_triggered.py
@@ -0,0 +1,13 @@
+API_STAGE = 'dev'
+APP_FUNCTION = 'app'
+APP_MODULE = 'tests.test_wsgi_script_name_app'
+BINARY_SUPPORT = False
+CONTEXT_HEADER_MAPPINGS = {}
+DEBUG = 'True'
+DJANGO_SETTINGS = None
+DOMAIN = 'api.example.com'
+ENVIRONMENT_VARIABLES = {}
+LOG_LEVEL = 'DEBUG'
+PROJECT_NAME = 'wsgi_script_name_settings'
+COGNITO_TRIGGER_MAPPING = {}
+AWS_BOT_EVENT_MAPPING = {'intent-name:DialogCodeHook': 'tests.test_handler.handle_bot_intent'}
diff --git a/tests/test_handler.py b/tests/test_handler.py
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -21,10 +21,15 @@ def var_args(*args):
 def var_args_with_one(first, *args):
     return first, args[0]
 
+
 def unsupported(first, second, third):
     return first, second, third
 
 
+def handle_bot_intent(event, context):
+    return "Success"
+
+
 class TestZappa(unittest.TestCase):
 
     def test_run_function(self):
@@ -142,7 +147,7 @@ def test_wsgi_script_on_cognito_event_request(self):
                                    'clientId': 'client-id-here'},
                  'triggerSource': 'PreSignUp_SignUp',
                  'request': {'userAttributes':
-                             {'email': 'email@example.com'}, 'validationData': None},
+                                 {'email': 'email@example.com'}, 'validationData': None},
                  'response': {'autoConfirmUser': False,
                               'autoVerifyEmail': False,
                               'autoVerifyPhone': False}}
@@ -150,3 +155,38 @@ def test_wsgi_script_on_cognito_event_request(self):
         response = lh.handler(event, None)
 
         self.assertEqual(response['response']['autoConfirmUser'], False)
+
+    def test_bot_triggered_event(self):
+        """
+        Ensure that bot triggered events are handled as in the settings
+        """
+        lh = LambdaHandler('tests.test_bot_handler_being_triggered')
+        # from : https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-lex
+        event = {
+            "messageVersion": "1.0",
+            "invocationSource": "DialogCodeHook",
+            "userId": "user-id specified in the POST request to Amazon Lex.",
+            "sessionAttributes": {
+                "key1": "value1",
+                "key2": "value2",
+            },
+            "bot": {
+                "name": "bot-name",
+                "alias": "bot-alias",
+                "version": "bot-version"
+            },
+            "outputDialogMode": "Text or Voice, based on ContentType request header in runtime API request",
+            "currentIntent": {
+                "name": "intent-name",
+                "slots": {
+                    "slot-name": "value",
+                    "slot-name": "value",
+                    "slot-name": "value"
+                },
+                "confirmationStatus": "None, Confirmed, or Denied (intent confirmation, if configured)"
+            }
+        }
+
+        response = lh.handler(event, None)
+
+        self.assertEqual(response, 'Success')