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