]> git.cworth.org Git - turbot/blob - turbot_lambda/turbot_lambda.py
Combine actions.py and commands.py into interaction.py
[turbot] / turbot_lambda / turbot_lambda.py
1 from urllib.parse import parse_qs
2 from slack import WebClient
3 import base64
4 import boto3
5 import requests
6 import json
7 import os
8 import pickle
9 from types import SimpleNamespace
10 from google.auth.transport.requests import Request
11 from googleapiclient.discovery import build
12
13 import turbot.interaction
14 import turbot.events
15
16 ssm = boto3.client('ssm')
17
18 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
19 slack_signing_secret = response['Parameter']['Value']
20 os.environ['SLACK_SIGNING_SECRET'] = slack_signing_secret
21
22 # Note: Late import here to have the environment variable above available
23 from turbot.slack import slack_is_valid_request # noqa
24
25 response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True)
26 slack_bot_token = response['Parameter']['Value']
27 slack_client = WebClient(slack_bot_token)
28
29 response = ssm.get_parameter(Name='GSHEETS_PICKLE_BASE64', WithDecryption=True)
30 gsheets_pickle_base64 = response['Parameter']['Value']
31 gsheets_pickle = base64.b64decode(gsheets_pickle_base64)
32 gsheets_creds = pickle.loads(gsheets_pickle)
33 if gsheets_creds:
34     if gsheets_creds.valid:
35         print("Loaded valid GSheets credentials from SSM")
36     else:
37         gsheets_creds.refresh(Request())
38         gsheets_pickle = pickle.dumps(gsheets_creds)
39         gsheets_pickle_base64_bytes = base64.b64encode(gsheets_pickle)
40         gsheets_pickle_base64 = gsheets_pickle_base64_bytes.decode('us-ascii')
41         print("Storing refreshed GSheets credentials into SSM")
42         ssm.put_parameter(Name='GSHEETS_PICKLE_BASE64',
43                           Type='SecureString',
44                           Value=gsheets_pickle_base64,
45                           Overwrite=True)
46 service = build('sheets',
47                 'v4',
48                 credentials=gsheets_creds,
49                 cache_discovery=False)
50 sheets = service.spreadsheets()
51
52 db = boto3.resource('dynamodb')
53
54 turb = SimpleNamespace()
55 turb.slack_client = slack_client
56 turb.db = db
57 turb.sheets = sheets
58
59 def error(message):
60     """Generate an error response for a Slack request
61
62     This will print the error message (so that it appears in CloudWatch
63     logs) and will then return a dictionary suitable for returning
64     as an error response."""
65
66     print("Error: {}.".format(message))
67
68     return {
69         'statusCode': 400,
70         'body': ''
71     }
72
73 def turbot_lambda(event, context):
74     """Top-level entry point for our lambda function.
75
76     This function first verifies that the request actually came from
77     Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
78     refuses to do anything if not.
79
80     Then this defers to either turbot_event_handler or
81     turbot_slash_command to do any real work.
82     """
83
84     headers = requests.structures.CaseInsensitiveDict(event['headers'])
85
86     signature = headers['X-Slack-Signature']
87     timestamp = headers['X-Slack-Request-Timestamp']
88
89     if not slack_is_valid_request(signature, timestamp, event['body']):
90         return error("Invalid Slack signature")
91
92     # It's a bit cheesy, but we'll just use the content-type header to
93     # determine if we're being called from a Slack event or from a
94     # slash command or other interactivity. (The more typical way to
95     # do this would be to have different URLs for each Slack entry
96     # point, but it's simpler to have our Slack app implemented as a
97     # single AWS Lambda, (which can only have a single entry point).
98     content_type = headers['content-type']
99
100     if (content_type == "application/json"):
101         return turbot_event_handler(turb, event, context)
102     if (content_type == "application/x-www-form-urlencoded"):
103         return turbot_interactive_or_slash_command(turb, event, context)
104     return error("Unknown content-type: {}".format(content_type))
105
106 def turbot_event_handler(turb, event, context):
107     """Handler for all subscribed Slack events"""
108
109     body = json.loads(event['body'])
110
111     type = body['type']
112
113     if type == 'url_verification':
114         return url_verification_handler(turb, body)
115     if type == 'event_callback':
116         return event_callback_handler(turb, body)
117     return error("Unknown event type: {}".format(type))
118
119 def url_verification_handler(turb, body):
120
121     # First, we have to properly respond to url_verification
122     # challenges or else Slack won't let us configure our URL as an
123     # event handler.
124     challenge = body['challenge']
125
126     return {
127         'statusCode': 200,
128         'body': challenge
129     }
130
131 def event_callback_handler(turb, body):
132     type = body['event']['type']
133
134     if type in turbot.events.events:
135         return turbot.events.events[type](turb, body)
136     return error("Unknown event type: {}".format(type))
137
138 def turbot_interactive_or_slash_command(turb, event, context):
139     """Handler for Slack interactive things (buttons, shortcuts, etc.)
140     as well as slash commands.
141
142     This function simply makes a quiuck determination of what we're looking
143     at and then defers to either turbot_interactive or turbot_slash_command."""
144
145     # Both interactives and slash commands have a urlencoded body
146     body = parse_qs(event['body'])
147
148     # The difference is that an interactive thingy has a 'payload'
149     # while a slash command has a 'command'
150     if 'payload' in body:
151         return turbot_interactive(turb, json.loads(body['payload'][0]))
152     if 'command' in body:
153         return turbot_slash_command(turb, body)
154     return error("Unrecognized event (neither interactive nor slash command)")
155
156 def turbot_interactive(turb, payload):
157     """Handler for Slack interactive requests
158
159     These are the things that come from a user interacting with a button
160     a shortcut or some other interactive element that our app has made
161     available to the user."""
162
163     type = payload['type']
164
165     if type == 'block_actions':
166         return turbot_block_action(turb, payload)
167     if type == 'view_submission':
168         return turbot.interaction.view_submission(turb, payload)
169     if type == 'shortcut':
170         return turbot_shortcut(turb, payload);
171     return error("Unrecognized interactive type: {}".format(type))
172
173 def turbot_block_action(turb, payload):
174     """Handler for Slack interactive block actions
175
176     Specifically, those that have a payload type of 'block_actions'"""
177
178     actions = payload['actions']
179
180     if len(actions) != 1:
181         return error("No support for multiple actions ({}) in a single request"
182                      .format(len(actions)))
183
184     action = actions[0]
185
186     atype = action['type']
187     avalue = action['value']
188
189     if (
190             atype in turbot.interaction.actions
191             and avalue in turbot.interaction.actions[atype]
192     ):
193         return turbot.interaction.actions[atype][avalue](turb, payload)
194     return error("Unknown action of type/value: {}/{}".format(atype, avalue))
195
196 def turbot_shortcut(turb, payload):
197     """Handler for Slack shortcuts
198
199     These are invoked as either global or message shortcuts by a user."""
200
201     print("In turbot_shortcut, payload is: {}".format(str(payload)))
202
203     return error("Shortcut interactions not yet implemented")
204
205 def turbot_slash_command(turb, body):
206     """Implementation for Slack slash commands.
207
208     This parses the request and arguments and farms out to
209     supporting functions to implement all supported slash commands.
210     """
211
212     command = body['command'][0]
213     if 'text' in body:
214         args = body['text'][0]
215     else:
216         args = ''
217
218     if command in turbot.interaction.commands:
219         return turbot.interation.commands[command](turb, body, args)
220
221     return error("Command {} not implemented".format(command))