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