1 from urllib.parse import parse_qs
2 from turbot.rot import rot
5 from slack import WebClient
12 ssm = boto3.client('ssm')
14 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
15 slack_signing_secret = bytes(response['Parameter']['Value'], 'utf-8')
17 response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True)
18 slack_bot_token = response['Parameter']['Value']
19 slack_client = WebClient(slack_bot_token)
22 """Generate an error response for a Slack request
24 This will print the error message (so that it appears in CloudWatch
25 logs) and will then return a dictionary suitable for returning
26 as an error response."""
28 print("Error: {}.".format(message))
35 def slack_is_valid_request(slack_signature, timestamp, body):
36 """Returns True if the timestamp and body correspond to signature.
38 This implements the Slack signature verification using the slack
39 signing secret (obtained via an SSM parameter in code above)."""
41 content = "v0:{}:{}".format(timestamp, body).encode('utf-8')
43 signature = 'v0=' + hmac.new(slack_signing_secret,
45 hashlib.sha256).hexdigest()
47 if hmac.compare_digest(signature, slack_signature):
50 print("Bad signature: {} != {}".format(signature, slack_signature))
53 def turbot_lambda(event, context):
54 """Top-level entry point for our lambda function.
56 This function first verifies that the request actually came from
57 Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
58 refuses to do anything if not.
60 Then this defers to either turbot_event_handler or
61 turbot_slash_command to do any real work.
64 headers = requests.structures.CaseInsensitiveDict(event['headers'])
66 signature = headers['X-Slack-Signature']
67 timestamp = headers['X-Slack-Request-Timestamp']
69 if not slack_is_valid_request(signature, timestamp, event['body']):
70 return error("Invalid Slack signature")
72 # It's a bit cheesy, but we'll just use the content-type header to
73 # determine if we're being called from a Slack event or from a
74 # slash command or other interactivity. (The more typical way to
75 # do this would be to have different URLs for each Slack entry
76 # point, but it's simpler to have our Slack app implemented as a
77 # single AWS Lambda, (which can only have a single entry point).
78 content_type = headers['content-type']
80 if (content_type == "application/json"):
81 return turbot_event_handler(event, context)
82 if (content_type == "application/x-www-form-urlencoded"):
83 return turbot_interactive_or_slash_command(event, context)
84 return error("Unknown content-type: {}".format(content_type))
86 def turbot_event_handler(event, context):
87 """Handler for all subscribed Slack events"""
89 body = json.loads(event['body'])
93 if type == 'url_verification':
94 return url_verification_handler(body)
95 if type == 'event_callback':
96 return event_callback_handler(body)
97 return error("Unknown event type: {}".format(type))
99 def url_verification_handler(body):
101 # First, we have to properly respond to url_verification
102 # challenges or else Slack won't let us configure our URL as an
104 challenge = body['challenge']
111 def event_callback_handler(body):
112 type = body['event']['type']
114 if type == 'app_home_opened':
115 return app_home_opened_handler(body)
116 return error("Unknown event type: {}".format(type))
118 def app_home_opened_handler(body):
119 user_id = body['event']['user']
120 view = turbot.views.home(user_id, body)
121 slack_client.views_publish(user_id=user_id, view=view)
124 def turbot_interactive_or_slash_command(event, context):
125 """Handler for Slack interactive things (buttons, shortcuts, etc.)
126 as well as slash commands.
128 This function simply makes a quiuck determination of what we're looking
129 at and then defers to either turbot_interactive or turbot_slash_command."""
131 # Both interactives and slash commands have a urlencoded body
132 body = parse_qs(event['body'])
134 # The difference is that an interactive thingy has a 'payload'
135 # while a slash command has a 'command'
136 if 'payload' in body:
137 return turbot_interactive(json.loads(body['payload'][0]))
138 if 'command' in body:
139 return turbot_slash_command(body)
140 return error("Unrecognized event (neither interactive nor slash command)")
142 def turbot_interactive(payload):
143 """Handler for Slack interactive requests
145 These are the things that come from a user interacting with a button
146 a shortcut or some other interactive element that our app has made
147 available to the user."""
149 type = payload['type']
151 if type == 'block_actions':
152 return turbot_block_action(payload)
153 return error("Unrecognized interactive type: {}".format(type))
155 def turbot_block_action(payload):
156 """Handler for Slack interactive block actions
158 Specifically, those that have a payload type of 'block_actions'"""
160 actions = payload['actions']
162 if len(actions) != 1:
163 return error("No support for multiple actions ({}) in a single request"
164 .format(len(actions)))
168 atype = action['type']
169 avalue = action['value']
171 if atype == 'button' and avalue == 'new_hunt':
172 return turbot.actions.new_hunt(payload)
173 return error("Unknown action of type/value: {}/{}".format(atype, avalue))
175 def turbot_slash_command(body):
176 """Implementation for Slack slash commands.
178 This parses the request and arguments and farms out to
179 supporting functions to implement all supported slash commands.
182 command = body['command'][0]
183 args = body['text'][0]
185 if (command == "/rotlambda" or command == "/rot"):
186 return rot_slash_command(body, args)
188 return error("Command {} not implemented".format(command))
190 def rot_slash_command(body, args):
191 """Implementation of the /rot command
193 The args string should be as follows:
195 [count|*] String to be rotated
197 That is, the first word of the string is an optional number (or
198 the character '*'). If this is a number it indicates an amount to
199 rotate each character in the string. If the count is '*' or is not
200 present, then the string will be rotated through all possible 25
203 The result of the rotation is returned (with Slack formatting) in
204 the body of the response so that Slack will provide it as a reply
205 to the user who submitted the slash command."""
207 channel_name = body['channel_name'][0]
208 response_url = body['response_url'][0]
209 channel_id = body['channel_id'][0]
213 if (channel_name == "directmessage"):
214 requests.post(response_url,
215 json = {"text": result},
216 headers = {"Content-type": "application/json"})
218 slack_client.chat_postMessage(channel=channel_id, text=result)