1 from urllib.parse import parse_qs
2 from slack import WebClient
7 from turbot.rot import rot
11 ssm = boto3.client('ssm')
13 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
14 slack_signing_secret = response['Parameter']['Value']
15 os.environ['SLACK_SIGNING_SECRET'] = slack_signing_secret
17 # Note: Late import here to have the environment variable above available
18 from turbot.slack import slack_is_valid_request # noqa
20 response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True)
21 slack_bot_token = response['Parameter']['Value']
22 slack_client = WebClient(slack_bot_token)
25 """Generate an error response for a Slack request
27 This will print the error message (so that it appears in CloudWatch
28 logs) and will then return a dictionary suitable for returning
29 as an error response."""
31 print("Error: {}.".format(message))
38 def turbot_lambda(event, context):
39 """Top-level entry point for our lambda function.
41 This function first verifies that the request actually came from
42 Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
43 refuses to do anything if not.
45 Then this defers to either turbot_event_handler or
46 turbot_slash_command to do any real work.
49 headers = requests.structures.CaseInsensitiveDict(event['headers'])
51 signature = headers['X-Slack-Signature']
52 timestamp = headers['X-Slack-Request-Timestamp']
54 if not slack_is_valid_request(signature, timestamp, event['body']):
55 return error("Invalid Slack signature")
57 # It's a bit cheesy, but we'll just use the content-type header to
58 # determine if we're being called from a Slack event or from a
59 # slash command or other interactivity. (The more typical way to
60 # do this would be to have different URLs for each Slack entry
61 # point, but it's simpler to have our Slack app implemented as a
62 # single AWS Lambda, (which can only have a single entry point).
63 content_type = headers['content-type']
65 if (content_type == "application/json"):
66 return turbot_event_handler(event, context)
67 if (content_type == "application/x-www-form-urlencoded"):
68 return turbot_interactive_or_slash_command(event, context)
69 return error("Unknown content-type: {}".format(content_type))
71 def turbot_event_handler(event, context):
72 """Handler for all subscribed Slack events"""
74 body = json.loads(event['body'])
78 if type == 'url_verification':
79 return url_verification_handler(body)
80 if type == 'event_callback':
81 return event_callback_handler(body)
82 return error("Unknown event type: {}".format(type))
84 def url_verification_handler(body):
86 # First, we have to properly respond to url_verification
87 # challenges or else Slack won't let us configure our URL as an
89 challenge = body['challenge']
96 def event_callback_handler(body):
97 type = body['event']['type']
99 if type == 'app_home_opened':
100 return app_home_opened_handler(body)
101 return error("Unknown event type: {}".format(type))
103 def app_home_opened_handler(body):
104 user_id = body['event']['user']
105 view = turbot.views.home(user_id, body)
106 slack_client.views_publish(user_id=user_id, view=view)
109 def turbot_interactive_or_slash_command(event, context):
110 """Handler for Slack interactive things (buttons, shortcuts, etc.)
111 as well as slash commands.
113 This function simply makes a quiuck determination of what we're looking
114 at and then defers to either turbot_interactive or turbot_slash_command."""
116 # Both interactives and slash commands have a urlencoded body
117 body = parse_qs(event['body'])
119 # The difference is that an interactive thingy has a 'payload'
120 # while a slash command has a 'command'
121 if 'payload' in body:
122 return turbot_interactive(json.loads(body['payload'][0]))
123 if 'command' in body:
124 return turbot_slash_command(body)
125 return error("Unrecognized event (neither interactive nor slash command)")
127 def turbot_interactive(payload):
128 """Handler for Slack interactive requests
130 These are the things that come from a user interacting with a button
131 a shortcut or some other interactive element that our app has made
132 available to the user."""
134 type = payload['type']
136 if type == 'block_actions':
137 return turbot_block_action(payload)
138 return error("Unrecognized interactive type: {}".format(type))
140 def turbot_block_action(payload):
141 """Handler for Slack interactive block actions
143 Specifically, those that have a payload type of 'block_actions'"""
145 actions = payload['actions']
147 if len(actions) != 1:
148 return error("No support for multiple actions ({}) in a single request"
149 .format(len(actions)))
153 atype = action['type']
154 avalue = action['value']
156 if atype == 'button' and avalue == 'new_hunt':
157 return turbot.actions.new_hunt(payload)
158 return error("Unknown action of type/value: {}/{}".format(atype, avalue))
160 def turbot_slash_command(body):
161 """Implementation for Slack slash commands.
163 This parses the request and arguments and farms out to
164 supporting functions to implement all supported slash commands.
167 command = body['command'][0]
168 args = body['text'][0]
170 if (command == "/rotlambda" or command == "/rot"):
171 return rot_slash_command(body, args)
173 return error("Command {} not implemented".format(command))
175 def rot_slash_command(body, args):
176 """Implementation of the /rot command
178 The args string should be as follows:
180 [count|*] String to be rotated
182 That is, the first word of the string is an optional number (or
183 the character '*'). If this is a number it indicates an amount to
184 rotate each character in the string. If the count is '*' or is not
185 present, then the string will be rotated through all possible 25
188 The result of the rotation is returned (with Slack formatting) in
189 the body of the response so that Slack will provide it as a reply
190 to the user who submitted the slash command."""
192 channel_name = body['channel_name'][0]
193 response_url = body['response_url'][0]
194 channel_id = body['channel_id'][0]
198 if (channel_name == "directmessage"):
199 requests.post(response_url,
200 json = {"text": result},
201 headers = {"Content-type": "application/json"})
203 slack_client.chat_postMessage(channel=channel_id, text=result)