]> git.cworth.org Git - turbot/blob - turbot_lambda/turbot_lambda.py
Handle the submission of the "New Hunt" modal
[turbot] / turbot_lambda / turbot_lambda.py
1 from urllib.parse import parse_qs
2 from slack import WebClient
3 import boto3
4 import requests
5 import json
6 import os
7 from types import SimpleNamespace
8
9 import turbot.actions
10 import turbot.commands
11 import turbot.events
12
13 ssm = boto3.client('ssm')
14
15 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
16 slack_signing_secret = response['Parameter']['Value']
17 os.environ['SLACK_SIGNING_SECRET'] = slack_signing_secret
18
19 # Note: Late import here to have the environment variable above available
20 from turbot.slack import slack_is_valid_request # noqa
21
22 response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True)
23 slack_bot_token = response['Parameter']['Value']
24 slack_client = WebClient(slack_bot_token)
25
26 db = boto3.resource('dynamodb')
27
28 turb = SimpleNamespace()
29 turb.slack_client = slack_client
30 turb.db = db
31
32 def error(message):
33     """Generate an error response for a Slack request
34
35     This will print the error message (so that it appears in CloudWatch
36     logs) and will then return a dictionary suitable for returning
37     as an error response."""
38
39     print("Error: {}.".format(message))
40
41     return {
42         'statusCode': 400,
43         'body': ''
44     }
45
46 def turbot_lambda(event, context):
47     """Top-level entry point for our lambda function.
48
49     This function first verifies that the request actually came from
50     Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
51     refuses to do anything if not.
52
53     Then this defers to either turbot_event_handler or
54     turbot_slash_command to do any real work.
55     """
56
57     headers = requests.structures.CaseInsensitiveDict(event['headers'])
58
59     signature = headers['X-Slack-Signature']
60     timestamp = headers['X-Slack-Request-Timestamp']
61
62     if not slack_is_valid_request(signature, timestamp, event['body']):
63         return error("Invalid Slack signature")
64
65     # It's a bit cheesy, but we'll just use the content-type header to
66     # determine if we're being called from a Slack event or from a
67     # slash command or other interactivity. (The more typical way to
68     # do this would be to have different URLs for each Slack entry
69     # point, but it's simpler to have our Slack app implemented as a
70     # single AWS Lambda, (which can only have a single entry point).
71     content_type = headers['content-type']
72
73     if (content_type == "application/json"):
74         return turbot_event_handler(turb, event, context)
75     if (content_type == "application/x-www-form-urlencoded"):
76         return turbot_interactive_or_slash_command(turb, event, context)
77     return error("Unknown content-type: {}".format(content_type))
78
79 def turbot_event_handler(turb, event, context):
80     """Handler for all subscribed Slack events"""
81
82     body = json.loads(event['body'])
83
84     type = body['type']
85
86     if type == 'url_verification':
87         return url_verification_handler(turb, body)
88     if type == 'event_callback':
89         return event_callback_handler(turb, body)
90     return error("Unknown event type: {}".format(type))
91
92 def url_verification_handler(turb, body):
93
94     # First, we have to properly respond to url_verification
95     # challenges or else Slack won't let us configure our URL as an
96     # event handler.
97     challenge = body['challenge']
98
99     return {
100         'statusCode': 200,
101         'body': challenge
102     }
103
104 def event_callback_handler(turb, body):
105     type = body['event']['type']
106
107     if type in turbot.events.events:
108         return turbot.events.events[type](turb, body)
109     return error("Unknown event type: {}".format(type))
110
111 def turbot_interactive_or_slash_command(turb, event, context):
112     """Handler for Slack interactive things (buttons, shortcuts, etc.)
113     as well as slash commands.
114
115     This function simply makes a quiuck determination of what we're looking
116     at and then defers to either turbot_interactive or turbot_slash_command."""
117
118     # Both interactives and slash commands have a urlencoded body
119     body = parse_qs(event['body'])
120
121     # The difference is that an interactive thingy has a 'payload'
122     # while a slash command has a 'command'
123     if 'payload' in body:
124         return turbot_interactive(turb, json.loads(body['payload'][0]))
125     if 'command' in body:
126         return turbot_slash_command(turb, body)
127     return error("Unrecognized event (neither interactive nor slash command)")
128
129 def turbot_interactive(turb, payload):
130     """Handler for Slack interactive requests
131
132     These are the things that come from a user interacting with a button
133     a shortcut or some other interactive element that our app has made
134     available to the user."""
135
136     type = payload['type']
137
138     if type == 'block_actions':
139         return turbot_block_action(turb, payload)
140     if type == 'view_submission':
141         return turbot.actions.view_submission(turb, payload)
142     return error("Unrecognized interactive type: {}".format(type))
143
144 def turbot_block_action(turb, payload):
145     """Handler for Slack interactive block actions
146
147     Specifically, those that have a payload type of 'block_actions'"""
148
149     actions = payload['actions']
150
151     if len(actions) != 1:
152         return error("No support for multiple actions ({}) in a single request"
153                      .format(len(actions)))
154
155     action = actions[0]
156
157     atype = action['type']
158     avalue = action['value']
159
160     if (
161             atype in turbot.actions.actions
162             and avalue in turbot.actions.actions[atype]
163     ):
164         return turbot.actions.actions[atype][avalue](turb, payload)
165     return error("Unknown action of type/value: {}/{}".format(atype, avalue))
166
167 def turbot_slash_command(turb, body):
168     """Implementation for Slack slash commands.
169
170     This parses the request and arguments and farms out to
171     supporting functions to implement all supported slash commands.
172     """
173
174     command = body['command'][0]
175     args = body['text'][0]
176
177     if command in turbot.commands.commands:
178         return turbot.commands.commands[command](turb, body, args)
179
180     return error("Command {} not implemented".format(command))