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