]> git.cworth.org Git - turbot/blob - turbot_lambda/turbot_lambda.py
b86d9184921c9a61fb67a4b1b89a97d4f308dd0e
[turbot] / turbot_lambda / turbot_lambda.py
1 from urllib.parse import parse_qs
2 from turbot.rot import rot
3 from slack import WebClient
4 import boto3
5 import requests
6 import hashlib
7 import hmac
8
9 ssm = boto3.client('ssm')
10
11 response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
12 slack_signing_secret = bytes(response['Parameter']['Value'], 'utf-8')
13
14 response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True)
15 slack_bot_token = response['Parameter']['Value']
16 slack_client = WebClient(slack_bot_token)
17
18 def error(message):
19     """Generate an error response for a Slack request
20
21     This will print the error message (so that it appears in CloudWatch
22     logs) and will then return a dictionary suitable for returning
23     as an error response."""
24
25     print("Error: {}.".format(message))
26
27     return {
28         'statusCode': 400,
29         'body': ''
30     }
31
32 def slack_is_valid_request(slack_signature, timestamp, body):
33     """Returns True if the timestamp and body correspond to signature.
34
35     This implements the Slack signature verification using the slack
36     signing secret (obtained via an SSM parameter in code above)."""
37
38     content = "v0:{}:{}".format(timestamp,body).encode('utf-8')
39
40     signature = 'v0=' + hmac.new(slack_signing_secret,
41                                  content,
42                                  hashlib.sha256).hexdigest()
43
44     if hmac.compare_digest(signature, slack_signature):
45         return True
46     else:
47         print("Bad signature: {} != {}".format(signature, slack_signature))
48         return False
49
50 def turbot_lambda(event, context):
51     """Top-level entry point for our lambda function.
52
53     This function first verifies that the request actually came from
54     Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
55     refuses to do anything if not.
56
57     Then this parses the request and arguments and farms out to
58     supporting functions to implement all supported slash commands.
59
60     """
61
62     signature = event['headers']['X-Slack-Signature']
63     timestamp = event['headers']['X-Slack-Request-Timestamp']
64
65     if not slack_is_valid_request(signature, timestamp, event['body']):
66         return error("Invalid Slack signature")
67
68     body = parse_qs(event['body'])
69     command = body['command'][0]
70     args = body['text'][0]
71
72     if (command == "/rotlambda" or command == "/rot"):
73         return rot_slash_command(body, args)
74
75     return error("Command {} not implemented".format(command))
76
77 def rot_slash_command(body, args):
78     """Implementation of the /rot command
79
80     The args string should be as follows:
81
82         [count|*] String to be rotated
83
84     That is, the first word of the string is an optional number (or
85     the character '*'). If this is a number it indicates an amount to
86     rotate each character in the string. If the count is '*' or is not
87     present, then the string will be rotated through all possible 25
88     values.
89
90     The result of the rotation is returned (with Slack formatting) in
91     the body of the response so that Slack will provide it as a reply
92     to the user who submitted the slash command."""
93
94     channel_name = body['channel_name'][0]
95     response_url = body['response_url'][0]
96     channel_id = body['channel_id'][0]
97
98     result = rot(args)
99
100     if (channel_name == "directmessage"):
101         requests.post(response_url,
102                       json = {"text": result},
103                       headers = {"Content-type": "application/json"})
104     else:
105         slack_client.chat_postMessage(channel=channel_id, text=result)
106
107     return {
108         'statusCode': 200,
109         'body': ""
110     }