]> git.cworth.org Git - turbot/blob - turbot_lambda/turbot_lambda.py
Use the Google drive API to create a hunt's sheet within a folder
[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 files = service.files()
63 permissions = service.permissions()
64
65 db = boto3.resource('dynamodb')
66
67 turb = SimpleNamespace()
68 turb.slack_client = slack_client
69 turb.db = db
70 turb.table = db.Table("turbot")
71 turb.sheets = sheets
72 turb.files = files
73 turb.permissions = permissions
74
75 def error(message):
76     """Generate an error response for a Slack request
77
78     This will print the error message (so that it appears in CloudWatch
79     logs) and will then return a dictionary suitable for returning
80     as an error response."""
81
82     print("Error: {}.".format(message))
83
84     return {
85         'statusCode': 400,
86         'body': ''
87     }
88
89 def turbot_lambda(event, context):
90     """Top-level entry point for our lambda function.
91
92     This function first verifies that the request actually came from
93     Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
94     refuses to do anything if not.
95
96     Then this defers to either turbot_event_handler or
97     turbot_slash_command to do any real work.
98     """
99
100     headers = requests.structures.CaseInsensitiveDict(event['headers'])
101
102     signature = headers['X-Slack-Signature']
103     timestamp = headers['X-Slack-Request-Timestamp']
104
105     if not slack_is_valid_request(signature, timestamp, event['body']):
106         return error("Invalid Slack signature")
107
108     # It's a bit cheesy, but we'll just use the content-type header to
109     # determine if we're being called from a Slack event or from a
110     # slash command or other interactivity. (The more typical way to
111     # do this would be to have different URLs for each Slack entry
112     # point, but it's simpler to have our Slack app implemented as a
113     # single AWS Lambda, (which can only have a single entry point).
114     content_type = headers['content-type']
115
116     if (content_type == "application/json"):
117         return turbot_event_handler(turb, event, context)
118     if (content_type == "application/x-www-form-urlencoded"):
119         return turbot_interactive_or_slash_command(turb, event, context)
120     return error("Unknown content-type: {}".format(content_type))
121
122 def turbot_event_handler(turb, event, context):
123     """Handler for all subscribed Slack events"""
124
125     body = json.loads(event['body'])
126
127     type = body['type']
128
129     if type == 'url_verification':
130         return url_verification_handler(turb, body)
131     if type == 'event_callback':
132         return event_callback_handler(turb, body)
133     return error("Unknown event type: {}".format(type))
134
135 def url_verification_handler(turb, body):
136
137     # First, we have to properly respond to url_verification
138     # challenges or else Slack won't let us configure our URL as an
139     # event handler.
140     challenge = body['challenge']
141
142     return {
143         'statusCode': 200,
144         'body': challenge
145     }
146
147 def event_callback_handler(turb, body):
148     event = body['event']
149     type = event['type']
150
151     if type in turbot.events.events:
152         return turbot.events.events[type](turb, event)
153     return error("Unknown event type: {}".format(type))
154
155 def turbot_interactive_or_slash_command(turb, event, context):
156     """Handler for Slack interactive things (buttons, shortcuts, etc.)
157     as well as slash commands.
158
159     This function simply makes a quick determination of what we're looking
160     at and then defers to either turbot_interactive or turbot_slash_command."""
161
162     # Both interactives and slash commands have a urlencoded body
163     body = parse_qs(event['body'])
164
165     # The difference is that an interactive thingy has a 'payload'
166     # while a slash command has a 'command'
167     if 'payload' in body:
168         return turbot_interactive(turb, json.loads(body['payload'][0]))
169     if 'command' in body:
170         return turbot_slash_command(turb, body)
171     return error("Unrecognized event (neither interactive nor slash command)")
172
173 def turbot_interactive(turb, payload):
174     """Handler for Slack interactive requests
175
176     These are the things that come from a user interacting with a button
177     a shortcut or some other interactive element that our app has made
178     available to the user."""
179
180     type = payload['type']
181
182     if type == 'block_actions':
183         return turbot_block_action(turb, payload)
184     if type == 'view_submission':
185         return turbot.interaction.view_submission(turb, payload)
186     if type == 'shortcut':
187         return turbot_shortcut(turb, payload)
188     return error("Unrecognized interactive type: {}".format(type))
189
190 def turbot_block_action(turb, payload):
191     """Handler for Slack interactive block actions
192
193     Specifically, those that have a payload type of 'block_actions'"""
194
195     actions = payload['actions']
196
197     if len(actions) != 1:
198         return error("No support for multiple actions ({}) in a single request"
199                      .format(len(actions)))
200
201     action = actions[0]
202
203     atype = action['type']
204     if 'value' in action:
205         avalue = action['value']
206     else:
207         avalue = '*'
208
209     if (
210             atype in turbot.interaction.actions
211             and avalue in turbot.interaction.actions[atype]
212     ):
213         return turbot.interaction.actions[atype][avalue](turb, payload)
214     return error("Unknown action of type/value: {}/{}".format(atype, avalue))
215
216 def turbot_shortcut(turb, payload):
217     """Handler for Slack shortcuts
218
219     These are invoked as either global or message shortcuts by a user."""
220
221     print("In turbot_shortcut, payload is: {}".format(str(payload)))
222
223     return error("Shortcut interactions not yet implemented")
224
225 def turbot_slash_command(turb, body):
226     """Implementation for Slack slash commands.
227
228     This parses the request and arguments and farms out to
229     supporting functions to implement all supported slash commands.
230     """
231
232     command = body['command'][0]
233     if 'text' in body:
234         args = body['text'][0]
235     else:
236         args = ''
237
238     if command in turbot.interaction.commands:
239         return turbot.interaction.commands[command](turb, body, args)
240
241     return error("Command {} not implemented".format(command))