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