]> git.cworth.org Git - turbot/blob - turbot_lambda/turbot_lambda.py
Move rot function down into turbot/commands.py
[turbot] / turbot_lambda / turbot_lambda.py
1 from urllib.parse import parse_qs
2 from slack import WebClient
3 import boto3
4 import requests
5 import json
6 import os
7
8 import turbot.actions
9 import turbot.commands
10 import turbot.events
11 import turbot.views
12
13 ssm = boto3.client('ssm')
14
15 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
16 slack_signing_secret = response['Parameter']['Value']
17 os.environ['SLACK_SIGNING_SECRET'] = slack_signing_secret
18
19 # Note: Late import here to have the environment variable above available
20 from turbot.slack import slack_is_valid_request # noqa
21
22 response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True)
23 slack_bot_token = response['Parameter']['Value']
24 slack_client = WebClient(slack_bot_token)
25
26 def error(message):
27     """Generate an error response for a Slack request
28
29     This will print the error message (so that it appears in CloudWatch
30     logs) and will then return a dictionary suitable for returning
31     as an error response."""
32
33     print("Error: {}.".format(message))
34
35     return {
36         'statusCode': 400,
37         'body': ''
38     }
39
40 def turbot_lambda(event, context):
41     """Top-level entry point for our lambda function.
42
43     This function first verifies that the request actually came from
44     Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
45     refuses to do anything if not.
46
47     Then this defers to either turbot_event_handler or
48     turbot_slash_command to do any real work.
49     """
50
51     headers = requests.structures.CaseInsensitiveDict(event['headers'])
52
53     signature = headers['X-Slack-Signature']
54     timestamp = headers['X-Slack-Request-Timestamp']
55
56     if not slack_is_valid_request(signature, timestamp, event['body']):
57         return error("Invalid Slack signature")
58
59     # It's a bit cheesy, but we'll just use the content-type header to
60     # determine if we're being called from a Slack event or from a
61     # slash command or other interactivity. (The more typical way to
62     # do this would be to have different URLs for each Slack entry
63     # point, but it's simpler to have our Slack app implemented as a
64     # single AWS Lambda, (which can only have a single entry point).
65     content_type = headers['content-type']
66
67     if (content_type == "application/json"):
68         return turbot_event_handler(event, context)
69     if (content_type == "application/x-www-form-urlencoded"):
70         return turbot_interactive_or_slash_command(event, context)
71     return error("Unknown content-type: {}".format(content_type))
72
73 def turbot_event_handler(event, context):
74     """Handler for all subscribed Slack events"""
75
76     body = json.loads(event['body'])
77
78     type = body['type']
79
80     if type == 'url_verification':
81         return url_verification_handler(body)
82     if type == 'event_callback':
83         return event_callback_handler(body)
84     return error("Unknown event type: {}".format(type))
85
86 def url_verification_handler(body):
87
88     # First, we have to properly respond to url_verification
89     # challenges or else Slack won't let us configure our URL as an
90     # event handler.
91     challenge = body['challenge']
92
93     return {
94         'statusCode': 200,
95         'body': challenge
96     }
97
98 def event_callback_handler(body):
99     type = body['event']['type']
100
101     if type == 'app_home_opened':
102         return turbot.events.app_home_opened(slack_client, body)
103     return error("Unknown event type: {}".format(type))
104
105 def turbot_interactive_or_slash_command(event, context):
106     """Handler for Slack interactive things (buttons, shortcuts, etc.)
107     as well as slash commands.
108
109     This function simply makes a quiuck determination of what we're looking
110     at and then defers to either turbot_interactive or turbot_slash_command."""
111
112     # Both interactives and slash commands have a urlencoded body
113     body = parse_qs(event['body'])
114
115     # The difference is that an interactive thingy has a 'payload'
116     # while a slash command has a 'command'
117     if 'payload' in body:
118         return turbot_interactive(json.loads(body['payload'][0]))
119     if 'command' in body:
120         return turbot_slash_command(body)
121     return error("Unrecognized event (neither interactive nor slash command)")
122
123 def turbot_interactive(payload):
124     """Handler for Slack interactive requests
125
126     These are the things that come from a user interacting with a button
127     a shortcut or some other interactive element that our app has made
128     available to the user."""
129
130     type = payload['type']
131
132     if type == 'block_actions':
133         return turbot_block_action(payload)
134     return error("Unrecognized interactive type: {}".format(type))
135
136 def turbot_block_action(payload):
137     """Handler for Slack interactive block actions
138
139     Specifically, those that have a payload type of 'block_actions'"""
140
141     actions = payload['actions']
142
143     if len(actions) != 1:
144         return error("No support for multiple actions ({}) in a single request"
145                      .format(len(actions)))
146
147     action = actions[0]
148
149     atype = action['type']
150     avalue = action['value']
151
152     if atype == 'button' and avalue == 'new_hunt':
153         return turbot.actions.new_hunt(payload)
154     return error("Unknown action of type/value: {}/{}".format(atype, avalue))
155
156 def turbot_slash_command(body):
157     """Implementation for Slack slash commands.
158
159     This parses the request and arguments and farms out to
160     supporting functions to implement all supported slash commands.
161     """
162
163     command = body['command'][0]
164     args = body['text'][0]
165
166     if (command == "/rotlambda" or command == "/rot"):
167         return turbot.commands.rot(slack_client, body, args)
168
169     return error("Command {} not implemented".format(command))