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