Slack Tutorial - Slash Commands

This tutorial demonstrates using Cloud Functions to implement a Slack Slash Command that searches the Google Knowledge Graph API.

Objectives

  • Create a Slash Command in Slack.
  • Write and deploy an HTTP Cloud Function.
  • Search the Google Knowledge Graph API using the Slash Command.

Costs

This tutorial uses billable components of Cloud Platform, including:

  • Google Cloud Functions

Use the Pricing Calculator to generate a cost estimate based on your projected usage.

Before you begin

  1. Sign in to your Google Account.

    If you don't already have one, sign up for a new account.

  2. Select or create a GCP project.

    Go to the Manage resources page

  3. Make sure that billing is enabled for your project.

    Learn how to enable billing

  4. Enable the Cloud Functions and Google Knowledge Graph Search APIs.

    Enable the APIs

  5. Update and install gcloud components:

    Node.js 6

    gcloud components update

    Node.js 8 (Beta)

    gcloud components update &&
    gcloud components install beta

    Python (Beta)

    gcloud components update &&
    gcloud components install beta
  6. Prepare your development environment.

Visualizing the flow of data

The flow of data in the Slack Slash Command tutorial application involves several steps:

  1. The user executes the /kg <search_query> Slash Command in a Slack channel.
  2. Slack sends the command payload to the Cloud Function's trigger endpoint.
  3. The Cloud Function sends a request with the user's search query to the Knowledge Graph API.
  4. The Knowledge Graph API responds with any matching results.
  5. The Cloud Function formats the response into a Slack message.
  6. The Cloud Function sends the message back to Slack.
  7. The user sees the formatted response in the Slack channel.

It may help to visualize the steps:

Preparing the function

  1. Clone the sample app repository to your local machine:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    Alternatively, you can download the sample as a zip file and extract it.

    Python (Beta)

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    Alternatively, you can download the sample as a zip file and extract it.

  2. Change to the directory that contains the Cloud Functions sample code:

    Node.js

    cd nodejs-docs-samples/functions/slack/

    Python (Beta)

    cd python-docs-samples/functions/slack/

  3. Configure the app:

    Node.js

    Using the config.default.json file as a template, create a config.json file in the app directory with the following contents:
    {
    "SLACK_TOKEN": "YOUR_SLACK_TOKEN",
    "KG_API_KEY": "YOUR_KG_API_KEY",
    }
    • Replace YOUR_SLACK_TOKEN with the verification token provided by Slack in the Basic information page of your app configuration.
    • Replace YOUR_KG_API_KEY with the Knowledge Graph API Key you just created.

    Python (Beta)

    Edit the config.json file in the app directory to have the following contents:
    {
    "SLACK_TOKEN": "YOUR_SLACK_TOKEN",
    "KG_API_KEY": "YOUR_KG_API_KEY",
    }
    • Replace YOUR_SLACK_TOKEN with the verification token provided by Slack in the Basic information page of your app configuration.
    • Replace YOUR_KG_API_KEY with the Knowledge Graph API Key you just created.

Deploying the function

To deploy the function that is executed when you (or Slack) make an HTTP POST request to the function's endpoint, run the following command in the directory that contains the Cloud Functions sample code:

Node.js 6

gcloud functions deploy kgSearch --runtime nodejs6 --trigger-http

Node.js 8 (Beta)

gcloud functions deploy kgSearch --runtime nodejs8 --trigger-http

Python (Beta)

gcloud functions deploy kg_search --runtime python37 --trigger-http

Configuring the application

After the function is deployed, you need to create a Slack Slash Command that sends the query to your Cloud Function every time the command is triggered:

  1. Create a Slack App to host your Slack Slash Command. Associate it with a Slack team where you have permissions to install integrations.

  2. Go to Slash commands and click the Create new command button.

  3. Enter /kg as the name of the command.

  4. Enter the URL for the command:

    Node.js

    https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/kgSearch

    Python (Beta)

    https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/kg_search

    where YOUR_REGION is the region where your Cloud Function is deployed and YOUR_PROJECT_ID is your Cloud project ID.

    Both values are visible in your terminal when your function finishes deploying.

  5. Click Save.

  6. Go to Basic Information.

  7. Click Install your app to your workspace and follow the instructions on screen to enable the application for your workspace.

    Your Slack Slash Command should come online shortly.

Understanding the code

Importing dependencies

The application must import several dependencies in order to communicate with Google Cloud Platform services:

Node.js

const config = require('./config.json');
const googleapis = require('googleapis');

// Get a reference to the Knowledge Graph Search component
const kgsearch = googleapis.kgsearch('v1');

Python (Beta)

import json

import apiclient
from flask import jsonify

with open('config.json', 'r') as f:
    data = f.read()
config = json.loads(data)

kgsearch = apiclient.discovery.build('kgsearch', 'v1',
                                     developerKey=config['KG_API_KEY'])

Receiving the webhook

The following function is executed when you (or Slack) make an HTTP POST request to the function's endpoint:

Node.js

/**
 * Receive a Slash Command request from Slack.
 *
 * Trigger this function by making a POST request with a payload to:
 * https://[YOUR_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/kgsearch
 *
 * @example
 * curl -X POST "https://us-central1.your-project-id.cloudfunctions.net/kgSearch" --data '{"token":"[YOUR_SLACK_TOKEN]","text":"giraffe"}'
 *
 * @param {object} req Cloud Function request object.
 * @param {object} req.body The request payload.
 * @param {string} req.body.token Slack's verification token.
 * @param {string} req.body.text The user's search query.
 * @param {object} res Cloud Function response object.
 */
exports.kgSearch = (req, res) => {
  return Promise.resolve()
    .then(() => {
      if (req.method !== 'POST') {
        const error = new Error('Only POST requests are accepted');
        error.code = 405;
        throw error;
      }

      // Verify that this request came from Slack
      verifyWebhook(req.body);

      // Make the request to the Knowledge Graph Search API
      return makeSearchRequest(req.body.text);
    })
    .then((response) => {
      // Send the formatted message back to Slack
      res.json(response);
    })
    .catch((err) => {
      console.error(err);
      res.status(err.code || 500).send(err);
      return Promise.reject(err);
    });
};

Python (Beta)

def kg_search(request):
    if request.method != 'POST':
        return 'Only POST requests are accepted', 405

    verify_web_hook(request.form)
    kg_search_response = make_search_request(request.form['text'])
    return jsonify(kg_search_response)

The following function authenticates the incoming request by checking for the verification token generated by Slack:

Node.js

/**
 * Verify that the webhook request came from Slack.
 *
 * @param {object} body The body of the request.
 * @param {string} body.token The Slack token to be verified.
 */
function verifyWebhook (body) {
  if (!body || body.token !== config.SLACK_TOKEN) {
    const error = new Error('Invalid credentials');
    error.code = 401;
    throw error;
  }
}

Python (Beta)

def verify_web_hook(form):
    if not form or form.get('token') != config['SLACK_TOKEN']:
        raise ValueError('Invalid request/credentials.')

Querying the Knowledge Graph API

The following function sends a request with the user's search query to the Knowledge Graph API:

Node.js

/**
 * Send the user's search query to the Knowledge Graph API.
 *
 * @param {string} query The user's search query.
 */
function makeSearchRequest (query) {
  return new Promise((resolve, reject) => {
    kgsearch.entities.search({
      auth: config.KG_API_KEY,
      query: query,
      limit: 1
    }, (err, response) => {
      console.log(err);
      if (err) {
        reject(err);
        return;
      }

      // Return a formatted message
      resolve(formatSlackMessage(query, response));
    });
  });
}

Python (Beta)

def make_search_request(query):
    req = kgsearch.entities().search(query=query, limit=1)
    res = req.execute()
    return format_slack_message(query, res)

Formatting the Slack message

Finally, the following function formats the Knowledge Graph result into a richly formatted Slack message that will be displayed to the user:

Node.js

/**
 * Format the Knowledge Graph API response into a richly formatted Slack message.
 *
 * @param {string} query The user's search query.
 * @param {object} response The response from the Knowledge Graph API.
 * @returns {object} The formatted message.
 */
function formatSlackMessage (query, response) {
  let entity;

  // Extract the first entity from the result list, if any
  if (response && response.itemListElement && response.itemListElement.length > 0) {
    entity = response.itemListElement[0].result;
  }

  // Prepare a rich Slack message
  // See https://api.slack.com/docs/message-formatting
  const slackMessage = {
    response_type: 'in_channel',
    text: `Query: ${query}`,
    attachments: []
  };

  if (entity) {
    const attachment = {
      color: '#3367d6'
    };
    if (entity.name) {
      attachment.title = entity.name;
      if (entity.description) {
        attachment.title = `${attachment.title}: ${entity.description}`;
      }
    }
    if (entity.detailedDescription) {
      if (entity.detailedDescription.url) {
        attachment.title_link = entity.detailedDescription.url;
      }
      if (entity.detailedDescription.articleBody) {
        attachment.text = entity.detailedDescription.articleBody;
      }
    }
    if (entity.image && entity.image.contentUrl) {
      attachment.image_url = entity.image.contentUrl;
    }
    slackMessage.attachments.push(attachment);
  } else {
    slackMessage.attachments.push({
      text: 'No results match your query...'
    });
  }

  return slackMessage;
}

Python (Beta)

def format_slack_message(query, response):
    entity = None
    if response and response.get('itemListElement') is not None and \
       len(response['itemListElement']) > 0:
        entity = response['itemListElement'][0]['result']

    message = {
        'response_type': 'in_channel',
        'text': 'Query: {}'.format(query),
        'attachments': []
    }

    attachment = {}
    if entity:
        name = entity.get('name', '')
        description = entity.get('description', '')
        detailed_desc = entity.get('detailedDescription', {})
        url = detailed_desc.get('url')
        article = detailed_desc.get('articleBody')
        image_url = entity.get('image', {}).get('contentUrl')

        attachment['color'] = '#3367d6'
        if name and description:
            attachment['title'] = '{}: {}'.format(entity["name"],
                                                  entity["description"])
        elif name:
            attachment['title'] = name
        if url:
            attachment['title_link'] = url
        if article:
            attachment['text'] = article
        if image_url:
            attachment['image_url'] = image_url
    else:
        attachment['text'] = 'No results match your query.'
    message['attachments'].append(attachment)

    return message

Using the Slash command

  1. Test the command manually:

    Node.js

    curl -X POST "https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/kgSearch" -H "Content-Type: application/json" --data '{"token":"YOUR_SLACK_TOKEN","text":"giraffe"}'

    Python (Beta)

    curl -X POST "https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/kg_search" -H "Content-Type: application/json" --data '{"token":"YOUR_SLACK_TOKEN","text":"giraffe"}'

    where

    • YOUR_REGION is the region where your function is deployed. This is visible in your terminal when your function finishes deploying.
    • YOUR_PROJECT_ID is your Cloud project ID. This is visible in your terminal when your function finishes deploying.
    • YOUR_SLACK_TOKEN is the token provided by Slack in your Slash Command configuration.
  2. Watch the logs to be sure the executions have completed:

    gcloud functions logs read --limit 100
    

  3. Type the command into your Slack channel:

    /kg giraffe

Cleaning up

To avoid incurring charges to your Google Cloud Platform account for the resources used in this tutorial:

Deleting the project

The easiest way to eliminate billing is to delete the project you created for the tutorial.

To delete the project:

  1. In the GCP Console, go to the Projects page.

    Go to the Projects page

  2. In the project list, select the project you want to delete and click Delete project. After selecting the checkbox next to the project name, click
      Delete project
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Deleting the Cloud Function

To delete the Cloud Function you deployed in this tutorial, run the following command:

Node.js

gcloud functions delete kgSearch 

Python (Beta)

gcloud functions delete kg_search 

You can also delete Cloud Functions from the Google Cloud Platform Console.

Was this page helpful? Let us know how we did:

Send feedback about...

Cloud Functions