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