1 from urllib.parse import parse_qs
2 from slack import WebClient
7 from turbot.rot import rot
12 ssm = boto3.client('ssm')
14 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
15 slack_signing_secret = response['Parameter']['Value']
16 os.environ['SLACK_SIGNING_SECRET'] = slack_signing_secret
18 # Note: Late import here to have the environment variable above available
19 from turbot.slack import slack_is_valid_request # noqa
21 response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True)
22 slack_bot_token = response['Parameter']['Value']
23 slack_client = WebClient(slack_bot_token)
26 """Generate an error response for a Slack request
28 This will print the error message (so that it appears in CloudWatch
29 logs) and will then return a dictionary suitable for returning
30 as an error response."""
32 print("Error: {}.".format(message))
39 def turbot_lambda(event, context):
40 """Top-level entry point for our lambda function.
42 This function first verifies that the request actually came from
43 Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
44 refuses to do anything if not.
46 Then this defers to either turbot_event_handler or
47 turbot_slash_command to do any real work.
50 headers = requests.structures.CaseInsensitiveDict(event['headers'])
52 signature = headers['X-Slack-Signature']
53 timestamp = headers['X-Slack-Request-Timestamp']
55 if not slack_is_valid_request(signature, timestamp, event['body']):
56 return error("Invalid Slack signature")
58 # It's a bit cheesy, but we'll just use the content-type header to
59 # determine if we're being called from a Slack event or from a
60 # slash command or other interactivity. (The more typical way to
61 # do this would be to have different URLs for each Slack entry
62 # point, but it's simpler to have our Slack app implemented as a
63 # single AWS Lambda, (which can only have a single entry point).
64 content_type = headers['content-type']
66 if (content_type == "application/json"):
67 return turbot_event_handler(event, context)
68 if (content_type == "application/x-www-form-urlencoded"):
69 return turbot_interactive_or_slash_command(event, context)
70 return error("Unknown content-type: {}".format(content_type))
72 def turbot_event_handler(event, context):
73 """Handler for all subscribed Slack events"""
75 body = json.loads(event['body'])
79 if type == 'url_verification':
80 return url_verification_handler(body)
81 if type == 'event_callback':
82 return event_callback_handler(body)
83 return error("Unknown event type: {}".format(type))
85 def url_verification_handler(body):
87 # First, we have to properly respond to url_verification
88 # challenges or else Slack won't let us configure our URL as an
90 challenge = body['challenge']
97 def event_callback_handler(body):
98 type = body['event']['type']
100 if type == 'app_home_opened':
101 return turbot.events.app_home_opened(slack_client, body)
102 return error("Unknown event type: {}".format(type))
104 def turbot_interactive_or_slash_command(event, context):
105 """Handler for Slack interactive things (buttons, shortcuts, etc.)
106 as well as slash commands.
108 This function simply makes a quiuck determination of what we're looking
109 at and then defers to either turbot_interactive or turbot_slash_command."""
111 # Both interactives and slash commands have a urlencoded body
112 body = parse_qs(event['body'])
114 # The difference is that an interactive thingy has a 'payload'
115 # while a slash command has a 'command'
116 if 'payload' in body:
117 return turbot_interactive(json.loads(body['payload'][0]))
118 if 'command' in body:
119 return turbot_slash_command(body)
120 return error("Unrecognized event (neither interactive nor slash command)")
122 def turbot_interactive(payload):
123 """Handler for Slack interactive requests
125 These are the things that come from a user interacting with a button
126 a shortcut or some other interactive element that our app has made
127 available to the user."""
129 type = payload['type']
131 if type == 'block_actions':
132 return turbot_block_action(payload)
133 return error("Unrecognized interactive type: {}".format(type))
135 def turbot_block_action(payload):
136 """Handler for Slack interactive block actions
138 Specifically, those that have a payload type of 'block_actions'"""
140 actions = payload['actions']
142 if len(actions) != 1:
143 return error("No support for multiple actions ({}) in a single request"
144 .format(len(actions)))
148 atype = action['type']
149 avalue = action['value']
151 if atype == 'button' and avalue == 'new_hunt':
152 return turbot.actions.new_hunt(payload)
153 return error("Unknown action of type/value: {}/{}".format(atype, avalue))
155 def turbot_slash_command(body):
156 """Implementation for Slack slash commands.
158 This parses the request and arguments and farms out to
159 supporting functions to implement all supported slash commands.
162 command = body['command'][0]
163 args = body['text'][0]
165 if (command == "/rotlambda" or command == "/rot"):
166 return rot_slash_command(body, args)
168 return error("Command {} not implemented".format(command))
170 def rot_slash_command(body, args):
171 """Implementation of the /rot command
173 The args string should be as follows:
175 [count|*] String to be rotated
177 That is, the first word of the string is an optional number (or
178 the character '*'). If this is a number it indicates an amount to
179 rotate each character in the string. If the count is '*' or is not
180 present, then the string will be rotated through all possible 25
183 The result of the rotation is returned (with Slack formatting) in
184 the body of the response so that Slack will provide it as a reply
185 to the user who submitted the slash command."""
187 channel_name = body['channel_name'][0]
188 response_url = body['response_url'][0]
189 channel_id = body['channel_id'][0]
193 if (channel_name == "directmessage"):
194 requests.post(response_url,
195 json = {"text": result},
196 headers = {"Content-type": "application/json"})
198 slack_client.chat_postMessage(channel=channel_id, text=result)