X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=turbot_lambda%2Fturbot_lambda.py;h=5420b33dc36726a7e63423f4bb08a678a5beea11;hb=9d0b128dcb0af08c9586d526e8ed1c01a57e6973;hp=cfde85dbc1a4a7bf541cb5de0591b26e5fffd56b;hpb=b0dd4f4f286d6a75c24e2dece947b18aede0790c;p=turbot diff --git a/turbot_lambda/turbot_lambda.py b/turbot_lambda/turbot_lambda.py index cfde85d..5420b33 100644 --- a/turbot_lambda/turbot_lambda.py +++ b/turbot_lambda/turbot_lambda.py @@ -1,14 +1,172 @@ -from turbot.rot import rot +from urllib.parse import parse_qs +from slack import WebClient +import boto3 +import requests +import json +import os + +import turbot.actions +import turbot.commands +import turbot.events +import turbot.views + +ssm = boto3.client('ssm') + +response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True) +slack_signing_secret = response['Parameter']['Value'] +os.environ['SLACK_SIGNING_SECRET'] = slack_signing_secret + +# Note: Late import here to have the environment variable above available +from turbot.slack import slack_is_valid_request # noqa + +response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True) +slack_bot_token = response['Parameter']['Value'] +slack_client = WebClient(slack_bot_token) + +def error(message): + """Generate an error response for a Slack request + + This will print the error message (so that it appears in CloudWatch + logs) and will then return a dictionary suitable for returning + as an error response.""" + + print("Error: {}.".format(message)) + + return { + 'statusCode': 400, + 'body': '' + } def turbot_lambda(event, context): """Top-level entry point for our lambda function. - Currently only calls into the rot() function but may become more - sophisticated later on.""" + This function first verifies that the request actually came from + Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and + refuses to do anything if not. + + Then this defers to either turbot_event_handler or + turbot_slash_command to do any real work. + """ + + headers = requests.structures.CaseInsensitiveDict(event['headers']) + + signature = headers['X-Slack-Signature'] + timestamp = headers['X-Slack-Request-Timestamp'] + + if not slack_is_valid_request(signature, timestamp, event['body']): + return error("Invalid Slack signature") + + # It's a bit cheesy, but we'll just use the content-type header to + # determine if we're being called from a Slack event or from a + # slash command or other interactivity. (The more typical way to + # do this would be to have different URLs for each Slack entry + # point, but it's simpler to have our Slack app implemented as a + # single AWS Lambda, (which can only have a single entry point). + content_type = headers['content-type'] + + if (content_type == "application/json"): + return turbot_event_handler(event, context) + if (content_type == "application/x-www-form-urlencoded"): + return turbot_interactive_or_slash_command(event, context) + return error("Unknown content-type: {}".format(content_type)) + +def turbot_event_handler(event, context): + """Handler for all subscribed Slack events""" + + body = json.loads(event['body']) + + type = body['type'] + + if type == 'url_verification': + return url_verification_handler(body) + if type == 'event_callback': + return event_callback_handler(body) + return error("Unknown event type: {}".format(type)) + +def url_verification_handler(body): - result = rot(event['text']) + # First, we have to properly respond to url_verification + # challenges or else Slack won't let us configure our URL as an + # event handler. + challenge = body['challenge'] return { 'statusCode': 200, - 'body': result + 'body': challenge } + +def event_callback_handler(body): + type = body['event']['type'] + + if type == 'app_home_opened': + return turbot.events.app_home_opened(slack_client, body) + return error("Unknown event type: {}".format(type)) + +def turbot_interactive_or_slash_command(event, context): + """Handler for Slack interactive things (buttons, shortcuts, etc.) + as well as slash commands. + + This function simply makes a quiuck determination of what we're looking + at and then defers to either turbot_interactive or turbot_slash_command.""" + + # Both interactives and slash commands have a urlencoded body + body = parse_qs(event['body']) + + # The difference is that an interactive thingy has a 'payload' + # while a slash command has a 'command' + if 'payload' in body: + return turbot_interactive(json.loads(body['payload'][0])) + if 'command' in body: + return turbot_slash_command(body) + return error("Unrecognized event (neither interactive nor slash command)") + +def turbot_interactive(payload): + """Handler for Slack interactive requests + + These are the things that come from a user interacting with a button + a shortcut or some other interactive element that our app has made + available to the user.""" + + type = payload['type'] + + if type == 'block_actions': + return turbot_block_action(payload) + return error("Unrecognized interactive type: {}".format(type)) + +def turbot_block_action(payload): + """Handler for Slack interactive block actions + + Specifically, those that have a payload type of 'block_actions'""" + + actions = payload['actions'] + + if len(actions) != 1: + return error("No support for multiple actions ({}) in a single request" + .format(len(actions))) + + action = actions[0] + + atype = action['type'] + avalue = action['value'] + + if ( + atype in turbot.actions.actions + and avalue in turbot.actions.actions[atype] + ): + return turbot.actions.actions[atype][avalue](payload) + return error("Unknown action of type/value: {}/{}".format(atype, avalue)) + +def turbot_slash_command(body): + """Implementation for Slack slash commands. + + This parses the request and arguments and farms out to + supporting functions to implement all supported slash commands. + """ + + command = body['command'][0] + args = body['text'][0] + + if command in turbot.commands.commands: + return turbot.commands.commands[command](slack_client, body, args) + + return error("Command {} not implemented".format(command))