API Gateway - Slack SlashCommand

SlashLogo

In this example, we’ll walk through creating a Slack Slash Command service. The source for this is the SpartaSlackbot repo.

Our initial command handler won’t be very sophisticated, but will show the steps necessary to provision and configure a Sparta AWS Gateway-enabled Lambda function.

Define the Lambda Function

This lambda handler is a bit more complicated than the other examples, primarily because of the Slack Integration requirements. The full source is:

////////////////////////////////////////////////////////////////////////////////
// Hello world event handler
//
func helloSlackbot(w http.ResponseWriter, r *http.Request) {
	logger, _ := r.Context().Value(sparta.ContextKeyLogger).(*logrus.Logger)

	// 1. Unmarshal the primary event
	decoder := json.NewDecoder(r.Body)
	defer r.Body.Close()
	var lambdaEvent slackLambdaJSONEvent
	err := decoder.Decode(&lambdaEvent)
	if err != nil {
		logger.Error("Failed to unmarshal event data: ", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	// 2. Conditionally unmarshal to get the Slack text.  See
	// https://api.slack.com/slash-commands
	// for the value name list
	requestParams := url.Values{}
	if bodyData, ok := lambdaEvent.Body.(string); ok {
		requestParams, err = url.ParseQuery(bodyData)
		if err != nil {
			logger.Error("Failed to parse query: ", err.Error())
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		logger.WithFields(logrus.Fields{
			"Values": requestParams,
		}).Info("Slack slashcommand values")
	} else {
		logger.Info("Event body empty")
	}

	// 3. Create the response
	// Slack formatting:
	// https://api.slack.com/docs/formatting
	responseText := "You talkin to me?"
	for _, eachLine := range requestParams["text"] {
		responseText += fmt.Sprintf("\n>>> %s", eachLine)
	}

	// 4. Setup the response object:
	// https://api.slack.com/slash-commands, "Responding to a command"
	responseData := sparta.ArbitraryJSONObject{
		"response_type": "in_channel",
		"text":          responseText,
	}
	// 5. Send it off
	responseBody, err := json.Marshal(responseData)
	if err != nil {
		logger.Error("Failed to marshal response: ", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
	w.Write(responseBody)
}

There are a couple of things to note in this code:

  1. Custom Event Type

    • The inbound Slack POST request is application/x-www-form-urlencoded data. However, our integration mapping mediates the API Gateway HTTPS request, transforming the public request into an integration request. The integration mapping wraps the raw POST body with the mapping envelope (so that we can access identity information, HTTP headers, etc.), which produces an inbound JSON request that includes a Body parameter. The Body string value is the raw inbound POST data. Since it’s application/x-www-form-urlencoded, to get the actual parameters we need to parse it:

              if bodyData, ok := lambdaEvent.Body.(string); ok {
                requestParams, err = url.ParseQuery(bodyData)
              

    • The lambda function extracts all Slack parameters and if defined, sends the text back with a bit of Slack Message Formatting (and some attitude, to be honest about it):

              responseText := "You talkin to me?"
              for _, eachLine := range requestParams["text"] {
                responseText += fmt.Sprintf("\n>>> %s", eachLine)
              }
              
  2. Custom Response

    • The Slack API expects a JSON formatted response, which is created in step 4:

              responseData := sparta.ArbitraryJSONObject{
            		"response_type": "in_channel",
            		"text":          responseText,
            	}
              

Create the API Gateway

With our lambda function defined, we need to setup an API Gateway so that it’s publicly available:

apiStage := sparta.NewStage("v1")
apiGateway := sparta.NewAPIGateway("SpartaSlackbot", apiStage)

The apiStage value implies that we want to deploy this API Gateway Rest API as part of Sparta’s provision step.

Create Lambda Binding & Resource

Next we create an sparta.LambdaAWSInfo struct that references the s3ItemInfo function:

func spartaLambdaFunctions(api *sparta.API) []*sparta.LambdaAWSInfo {
	var lambdaFunctions []*sparta.LambdaAWSInfo
	lambdaFn := sparta.HandleAWSLambda(sparta.LambdaName(helloSlackbot),
		http.HandlerFunc(helloSlackbot),
		iamDynamicRole)

	if nil != api {
		apiGatewayResource, _ := api.NewResource("/slack", lambdaFn)
		_, err := apiGatewayResource.NewMethod("POST", http.StatusCreated)
		if nil != err {
			panic("Failed to create /hello resource")
		}
	}
	return append(lambdaFunctions, lambdaFn)
}

A few items to note here:

  • We’re using an empty sparta.IAMRoleDefinition{} definition because our go lambda function doesn’t access any additional AWS services.
  • Our lambda function will be accessible at the /slack child path of the deployed API Gateway instance
  • Slack supports both GET and POST integration types, but we’re limiting our lambda function to POST only

Provision

With everything configured, we then configure our main() function to forward to Sparta:

func main() {
	// Register the function with the API Gateway
	apiStage := sparta.NewStage("v1")
	apiGateway := sparta.NewAPIGateway("SpartaSlackbot", apiStage)

	// Deploy it
	sparta.Main("SpartaSlackbot",
		fmt.Sprintf("Sparta app that responds to Slack commands"),
		spartaLambdaFunctions(apiGateway),
		apiGateway,
		nil)
}

and provision the service:

S3_BUCKET=<MY_S3_BUCKETNAME> go run slack.go --level info provision

Look for the Stack output section of the log, you’ll need the APIGatewayURL value to configure Slack in the next step.

INFO[0083] Stack output Description=API Gateway URL Key=APIGatewayURL Value=https://75mtsly44i.execute-api.us-west-2.amazonaws.com/v1
INFO[0083] Stack output Description=Sparta Home Key=SpartaHome Value=https://github.com/mweagle/Sparta
INFO[0083] Stack output Description=Sparta Version Key=SpartaVersion Value=0.1.3

Configure Slack

At this point our lambda function is deployed and is available through the API Gateway (https://75mtsly44i.execute-api.us-west-2.amazonaws.com/v1/slack in the current example).

The next step is to configure Slack with this custom integration:

  1. Visit https://slack.com/apps/build and choose the “Custom Integration” option:

    Custom integration

  2. On the next page, choose “Slash Commands”:

    Slash Commands

  3. The next screen is where you input the command that will trigger your lambda function. Enter /sparta

    Slash Chose Command

    • and click the “Add Slash Command Integration” button.
  4. Finally, scroll down the next page to the Integration Settings section and provide the API Gateway URL of your lambda function.

    Slash URL

    • Leave the Method field unchanged (it should be POST), to match how we configured the API Gateway entry above.
  5. Save it

    Save it

There are additional Slash Command Integration options, but for this example the URL option is sufficient to trigger our command.

Test

With everything configured, visit your team’s Slack room and verify the integration via /sparta slash command:

Sparta Response

Cleaning Up

Before moving on, remember to decommission the service via:

go run slack.go delete

Wrapping Up

This example provides a good overview of Sparta & Slack integration, including how to handle external requests that are not application/json formatted.