1 from urllib.parse import parse_qs
2 from turbot.rot import rot
3 from slack import WebClient
10 ssm = boto3.client('ssm')
12 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
13 slack_signing_secret = bytes(response['Parameter']['Value'], 'utf-8')
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)
20 """Generate an error response for a Slack request
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."""
26 print("Error: {}.".format(message))
33 def slack_is_valid_request(slack_signature, timestamp, body):
34 """Returns True if the timestamp and body correspond to signature.
36 This implements the Slack signature verification using the slack
37 signing secret (obtained via an SSM parameter in code above)."""
39 content = "v0:{}:{}".format(timestamp,body).encode('utf-8')
41 signature = 'v0=' + hmac.new(slack_signing_secret,
43 hashlib.sha256).hexdigest()
45 if hmac.compare_digest(signature, slack_signature):
48 print("Bad signature: {} != {}".format(signature, slack_signature))
51 def turbot_lambda(event, context):
52 """Top-level entry point for our lambda function.
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.
58 Then this defers to either turbot_event_handler or
59 turbot_slash_command to do any real work.
62 headers = requests.structures.CaseInsensitiveDict(event['headers'])
64 signature = headers['X-Slack-Signature']
65 timestamp = headers['X-Slack-Request-Timestamp']
67 if not slack_is_valid_request(signature, timestamp, event['body']):
68 return error("Invalid Slack signature")
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']
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))
84 def turbot_event_handler(event, context):
85 """Handler for all subscribed Slack events"""
87 body = json.loads(event['body'])
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))
97 def url_verification_handler(body):
99 # First, we have to properly respond to url_verification
100 # challenges or else Slack won't let us configure our URL as an
102 challenge = body['challenge']
109 def event_callback_handler(body):
110 type = body['event']['type']
112 if type == 'app_home_opened':
113 return app_home_opened_handler(body)
114 return error("Unknown event type: {}".format(type))
116 def app_home_opened_handler(body):
117 slack_client.views_publish(user_id=body['event']['user'],
125 "text": "A simple stack of blocks for the simple sample Block Kit Home tab."
134 "type": "plain_text",
142 "type": "plain_text",
153 def turbot_interactive_or_slash_command(event, context):
154 """Handler for Slack interactive things (buttons, shortcuts, etc.)
155 as well as slash commands.
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."""
160 # Both interactives and slash commands have a urlencoded body
161 body = parse_qs(event['body'])
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)")
171 def turbot_interactive(payload):
172 """Handler for Slack interactive requests
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."""
178 print("In turbot_interactive, payload is: {}".format(str(payload)))
180 def turbot_slash_command(body):
181 """Implementation for Slack slash commands.
183 This parses the request and arguments and farms out to
184 supporting functions to implement all supported slash commands.
187 command = body['command'][0]
188 args = body['text'][0]
190 if (command == "/rotlambda" or command == "/rot"):
191 return rot_slash_command(body, args)
193 return error("Command {} not implemented".format(command))
195 def rot_slash_command(body, args):
196 """Implementation of the /rot command
198 The args string should be as follows:
200 [count|*] String to be rotated
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
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."""
212 channel_name = body['channel_name'][0]
213 response_url = body['response_url'][0]
214 channel_id = body['channel_id'][0]
218 if (channel_name == "directmessage"):
219 requests.post(response_url,
220 json = {"text": result},
221 headers = {"Content-type": "application/json"})
223 slack_client.chat_postMessage(channel=channel_id, text=result)