]> git.cworth.org Git - turbot/blob - turbot_lambda/turbot_lambda.py
7622d82e46168cde94a539d42850d220d3866616
[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     if 'value' in action:
203         avalue = action['value']
204     else:
205         avalue = '*'
206
207     if (
208             atype in turbot.interaction.actions
209             and avalue in turbot.interaction.actions[atype]
210     ):
211         return turbot.interaction.actions[atype][avalue](turb, payload)
212     return error("Unknown action of type/value: {}/{}".format(atype, avalue))
213
214 def turbot_shortcut(turb, payload):
215     """Handler for Slack shortcuts
216
217     These are invoked as either global or message shortcuts by a user."""
218
219     print("In turbot_shortcut, payload is: {}".format(str(payload)))
220
221     return error("Shortcut interactions not yet implemented")
222
223 def turbot_slash_command(turb, body):
224     """Implementation for Slack slash commands.
225
226     This parses the request and arguments and farms out to
227     supporting functions to implement all supported slash commands.
228     """
229
230     command = body['command'][0]
231     if 'text' in body:
232         args = body['text'][0]
233     else:
234         args = ''
235
236     if command in turbot.interaction.commands:
237         return turbot.interaction.commands[command](turb, body, args)
238
239     return error("Command {} not implemented".format(command))