1 from urllib.parse import parse_qs
2 from turbot.rot import rot
3 from turbot import views
4 from slack import WebClient
11 ssm = boto3.client('ssm')
13 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
14 slack_signing_secret = bytes(response['Parameter']['Value'], 'utf-8')
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)
21 """Generate an error response for a Slack request
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."""
27 print("Error: {}.".format(message))
34 def slack_is_valid_request(slack_signature, timestamp, body):
35 """Returns True if the timestamp and body correspond to signature.
37 This implements the Slack signature verification using the slack
38 signing secret (obtained via an SSM parameter in code above)."""
40 content = "v0:{}:{}".format(timestamp,body).encode('utf-8')
42 signature = 'v0=' + hmac.new(slack_signing_secret,
44 hashlib.sha256).hexdigest()
46 if hmac.compare_digest(signature, slack_signature):
49 print("Bad signature: {} != {}".format(signature, slack_signature))
52 def turbot_lambda(event, context):
53 """Top-level entry point for our lambda function.
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.
59 Then this defers to either turbot_event_handler or
60 turbot_slash_command to do any real work.
63 headers = requests.structures.CaseInsensitiveDict(event['headers'])
65 signature = headers['X-Slack-Signature']
66 timestamp = headers['X-Slack-Request-Timestamp']
68 if not slack_is_valid_request(signature, timestamp, event['body']):
69 return error("Invalid Slack signature")
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']
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))
85 def turbot_event_handler(event, context):
86 """Handler for all subscribed Slack events"""
88 body = json.loads(event['body'])
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))
98 def url_verification_handler(body):
100 # First, we have to properly respond to url_verification
101 # challenges or else Slack won't let us configure our URL as an
103 challenge = body['challenge']
110 def event_callback_handler(body):
111 type = body['event']['type']
113 if type == 'app_home_opened':
114 return app_home_opened_handler(body)
115 return error("Unknown event type: {}".format(type))
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)
123 def turbot_interactive_or_slash_command(event, context):
124 """Handler for Slack interactive things (buttons, shortcuts, etc.)
125 as well as slash commands.
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."""
130 # Both interactives and slash commands have a urlencoded body
131 body = parse_qs(event['body'])
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)")
141 def turbot_interactive(payload):
142 """Handler for Slack interactive requests
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."""
148 print("In turbot_interactive, payload is: {}".format(str(payload)))
150 def turbot_slash_command(body):
151 """Implementation for Slack slash commands.
153 This parses the request and arguments and farms out to
154 supporting functions to implement all supported slash commands.
157 command = body['command'][0]
158 args = body['text'][0]
160 if (command == "/rotlambda" or command == "/rot"):
161 return rot_slash_command(body, args)
163 return error("Command {} not implemented".format(command))
165 def rot_slash_command(body, args):
166 """Implementation of the /rot command
168 The args string should be as follows:
170 [count|*] String to be rotated
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
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."""
182 channel_name = body['channel_name'][0]
183 response_url = body['response_url'][0]
184 channel_id = body['channel_id'][0]
188 if (channel_name == "directmessage"):
189 requests.post(response_url,
190 json = {"text": result},
191 headers = {"Content-type": "application/json"})
193 slack_client.chat_postMessage(channel=channel_id, text=result)