Skip to content
This repository was archived by the owner on Jul 14, 2023. It is now read-only.

Commit 0aac9bd

Browse files
committed
initial commit
0 parents  commit 0aac9bd

10 files changed

+305
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__pycache__
2+
.aws-sam
3+
packaged.yaml
4+
envars.json

README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# slack-slash-lambda
2+
This repo holds code and configuration for lambda based [slash commands](https://api.slack.com/slash-commands) for Slack.
3+
4+
# Pre-Requirements
5+
To deploy this you will need the following
6+
7+
1. AWS credentails
8+
2. aws cli
9+
3. aws-sam-cli
10+
4. S3 bucket
11+
5. Access in slack to create your own App
12+
13+
# Secrets
14+
Secrets are passed to the lambda via environment variables. These are set by CloudFormation
15+
via parameters. Add secrets to `envars.sample.json` file and rename it to `envars.json`.
16+
17+
The keys:
18+
* SESSIONISE_KEY: the API id you can generate in sessionize.com under API/Embed section
19+
* TITO_KEY: v3 API key generated in the user profile
20+
* SLACK_KEY: Slack's app signing secret key generated in the App credentails section
21+
22+
# Local testing
23+
Testing behaviour of the lambda can be done locally:
24+
25+
$ sam local invoke -n envars.json -e event_cfp.json
26+
27+
# Build and Deploy
28+
First build and package:
29+
30+
$ sam build
31+
$ sam package --s3-bucket <bucket> --output-template-file packaged.yaml
32+
33+
Now deploy with awscli:
34+
35+
$ cloudformation deploy --template-file packaged.yaml --stack-name <name> --capabilities CAPABILITY_IAM --parameter-overrides $(jq -r '.Function | to_entries | .[] | .key +"="+ .value ' envars.json)
36+
37+
If deployed successfully, you can list outputs with:
38+
39+
$ aws cloudformation describe-stacks --stack-name <name> --query "Stacks[0].Outputs"
40+
41+
# Configuring Slack
42+
43+
Create a new App in Slack. Under the Slash Command section add your commands.
44+
Set `Request URL` to point at the `OutputValue` of *Api* `OutputKey`.
45+

envars.sample.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"Function": {
3+
"SESSIONISE_KEY": "CHANGE ME",
4+
"TITO_KEY": "CHANGE ME",
5+
"SLACK_KEY": "CHANGE ME"
6+
}
7+
}

event_cfp.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"payload": "command=/cfp&response_url=local"
3+
}

event_tickets.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"payload": "command=/tickets&response_url=local"
3+
}

slack_slash/__init__.py

Whitespace-only changes.

slack_slash/app.py

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import os
2+
import hmac
3+
from urllib.parse import parse_qs
4+
from functools import partial
5+
import json
6+
7+
import requests
8+
9+
10+
def lambda_handler(event, context):
11+
"""
12+
lambda entry point
13+
"""
14+
print(f"event: {event}")
15+
16+
# verify request is from slack (skip when local)
17+
if "AWS_SAM_LOCAL" not in os.environ and not verify(
18+
key=os.environ["SLACK_KEY"].encode(),
19+
version="v0",
20+
timestamp=event["params"]["header"]["X-Slack-Request-Timestamp"],
21+
payload=event["payload"],
22+
signature=event["params"]["header"]["X-Slack-Signature"].split("=")[1],
23+
):
24+
print("failed to verify Slack's message signature")
25+
return
26+
27+
# parse payload params (url query style)
28+
params = parse_qs(event["payload"])
29+
response_url = params["response_url"][0]
30+
command = params["command"][0]
31+
32+
callback = partial(send, response_url)
33+
34+
# handle commands
35+
if command == "/cfp":
36+
get_sessions(callback, secret=os.environ["SESSIONISE_KEY"])
37+
elif command == "/tickets":
38+
get_tickets(
39+
callback,
40+
secret=os.environ["TITO_KEY"],
41+
org=os.environ["TITO_ORG"],
42+
slug=os.environ["TITO_EVENT"],
43+
)
44+
else:
45+
print("no command found")
46+
47+
48+
def get_sessions(callback, secret):
49+
"""
50+
get total submissions number and last 10 submissions titles
51+
"""
52+
try:
53+
r = requests.get(f"https://sessionize.com/api/v2/{secret}/view/sessions")
54+
except Exception as e:
55+
print("sessionize didn't respond correctly")
56+
raise e
57+
58+
sessions = r.json()[0]["sessions"]
59+
content = f":star2: Number of talk submissions: *{len(sessions)}*\n"
60+
content += "\n"
61+
content += "recent submissions:\n"
62+
63+
sorted_sess = sorted(sessions, key=lambda i: i["id"], reverse=True)
64+
for i, s in enumerate(sorted_sess[:5]):
65+
content += f"{i+1}. {s['title']}\n"
66+
67+
callback(content)
68+
69+
70+
def get_tickets(callback, secret, org, slug):
71+
"""
72+
get number of tickets sold
73+
"""
74+
try:
75+
r = requests.get(
76+
f"https://api.tito.io/v3/{org}/{slug}/tickets",
77+
headers={
78+
"Accept": "application/json",
79+
"Authorization": f"Token token={secret}",
80+
},
81+
)
82+
except Exception as e:
83+
print("ti.to chocked")
84+
raise e
85+
86+
tickets = r.json()["tickets"]
87+
content = f"tickets sold: {len(tickets)}"
88+
89+
callback(content)
90+
91+
92+
def send(url, content):
93+
payload = {"text": content}
94+
if url == "local":
95+
print(payload)
96+
return
97+
98+
try:
99+
r = requests.post(url, json=payload)
100+
except Exception as e:
101+
print("slack didn't like that")
102+
raise e
103+
104+
r.raise_for_status()
105+
106+
107+
def verify(*, key, version, timestamp, payload, signature):
108+
"""
109+
verify slack message.
110+
info: https://api.slack.com/docs/verifying-requests-from-slack#a_recipe_for_security
111+
"""
112+
msg = f"{version}:{timestamp}:{payload}".encode()
113+
h = hmac.new(key, msg, "sha256")
114+
115+
return hmac.compare_digest(h.hexdigest(), signature)

slack_slash/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
requests

swagger.yaml

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
swagger: "2.0"
3+
info:
4+
version: "v1.0"
5+
title: "Slack API Gateway"
6+
basePath: "/prd"
7+
schemes:
8+
- "https"
9+
paths:
10+
/:
11+
post:
12+
consumes:
13+
- "application/x-www-form-urlencoded"
14+
produces:
15+
- "application/json"
16+
parameters:
17+
- name: "InvocationType"
18+
in: "header"
19+
required: false
20+
type: "string"
21+
- in: "body"
22+
name: "Empty"
23+
required: true
24+
schema:
25+
$ref: "#/definitions/Empty"
26+
responses:
27+
200:
28+
description: "200 response"
29+
schema:
30+
$ref: "#/definitions/Empty"
31+
x-amazon-apigateway-integration:
32+
uri: "arn:aws:apigateway:ap-southeast-2:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-southeast-2:814957326217:function:SlackSlash/invocations"
33+
responses:
34+
default:
35+
statusCode: "200"
36+
requestParameters:
37+
integration.request.header.X-Amz-Invocation-Type: "'Event'"
38+
passthroughBehavior: "when_no_templates"
39+
httpMethod: "POST"
40+
requestTemplates:
41+
application/x-www-form-urlencoded: "## See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html\n\
42+
## This template will pass through all parameters including path, querystring,\
43+
\ header, stage variables, and context through to the integration endpoint\
44+
\ via the body/payload\n#set($allParams = $input.params())\n{\n\"payload\"\
45+
\ : $input.json('$'),\n\"params\" : {\n#foreach($type in $allParams.keySet())\n\
46+
\ #set($params = $allParams.get($type))\n\"$type\" : {\n #foreach($paramName\
47+
\ in $params.keySet())\n \"$paramName\" : \"$util.escapeJavaScript($params.get($paramName))\"\
48+
\n #if($foreach.hasNext),#end\n #end\n}\n #if($foreach.hasNext),#end\n\
49+
#end\n},\n\"stage-variables\" : {\n#foreach($key in $stageVariables.keySet())\n\
50+
\"$key\" : \"$util.escapeJavaScript($stageVariables.get($key))\"\n \
51+
\ #if($foreach.hasNext),#end\n#end\n},\n\"context\" : {\n \"account-id\"\
52+
\ : \"$context.identity.accountId\",\n \"api-id\" : \"$context.apiId\"\
53+
,\n \"api-key\" : \"$context.identity.apiKey\",\n \"authorizer-principal-id\"\
54+
\ : \"$context.authorizer.principalId\",\n \"caller\" : \"$context.identity.caller\"\
55+
,\n \"cognito-authentication-provider\" : \"$context.identity.cognitoAuthenticationProvider\"\
56+
,\n \"cognito-authentication-type\" : \"$context.identity.cognitoAuthenticationType\"\
57+
,\n \"cognito-identity-id\" : \"$context.identity.cognitoIdentityId\"\
58+
,\n \"cognito-identity-pool-id\" : \"$context.identity.cognitoIdentityPoolId\"\
59+
,\n \"http-method\" : \"$context.httpMethod\",\n \"stage\" : \"\
60+
$context.stage\",\n \"source-ip\" : \"$context.identity.sourceIp\"\
61+
,\n \"user\" : \"$context.identity.user\",\n \"user-agent\" : \"\
62+
$context.identity.userAgent\",\n \"user-arn\" : \"$context.identity.userArn\"\
63+
,\n \"request-id\" : \"$context.requestId\",\n \"resource-id\" :\
64+
\ \"$context.resourceId\",\n \"resource-path\" : \"$context.resourcePath\"\
65+
\n }\n}\n"
66+
contentHandling: "CONVERT_TO_TEXT"
67+
type: "aws"
68+
definitions:
69+
Empty:
70+
type: "object"
71+
title: "Empty Schema"

template.yaml

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: lambdas for slack
4+
5+
Parameters:
6+
SessioniseKey:
7+
Type: String
8+
NoEcho: true
9+
TitoKey:
10+
Type: String
11+
NoEcho: true
12+
SlackKey:
13+
Type: String
14+
NoEcho: true
15+
16+
Resources:
17+
APIGateway:
18+
Type: AWS::Serverless::Api
19+
Properties:
20+
StageName: prd
21+
DefinitionUri: ./swagger.yaml
22+
23+
Function:
24+
Type: AWS::Serverless::Function
25+
Properties:
26+
FunctionName: SlackSlash
27+
CodeUri: slack_slash/
28+
Handler: app.lambda_handler
29+
Runtime: python3.7
30+
Timeout: 5
31+
Environment:
32+
Variables:
33+
SESSIONISE_KEY: !Ref SessioniseKey
34+
TITO_ORG: devopsaustralia
35+
TITO_EVENT: sydney-2019
36+
TITO_KEY: !Ref TitoKey
37+
SLACK_KEY: !Ref SlackKey
38+
39+
Events:
40+
PostPetApi:
41+
Type: Api
42+
Properties:
43+
RestApiId: !Ref "APIGateway"
44+
Path: /
45+
Method: POST
46+
Policies:
47+
- AWSLambdaBasicExecutionRole
48+
49+
Outputs:
50+
Api:
51+
Description: "API Gateway endpoint URL for prd stage for Hello World function"
52+
Value: !Sub "https://${APIGateway}.execute-api.${AWS::Region}.amazonaws.com/prd/"
53+
Function:
54+
Description: "Hello World Lambda Function ARN"
55+
Value: !GetAtt Function.Arn
56+

0 commit comments

Comments
 (0)