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