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