Retrieve Record data with Slack's Slash Commands

Contents

Overview

This article introduces how to retrieve record data from Kintone Apps using Slack's Slash Commands (External link) . AWS Lambda is used to facilitate the integration.

Prepare the Slash Command on Slack

Navigate to https://api.slack.com/apps (External link) and click Create New App. Enter the required fields, and click Create App as shown below.

On the next page select Slash Commands, and then Create New Command. Enter the following values:

Field Value
Command /mytodo
Request URL (Enter a dummy URL for now)
Short Description List your todos

An API Gateway URL will be entered in the Request URL field later on, but for now a dummy URL can be set. Once the Slash Command has been created, navigate to Settings and then Basic Information and take note of the Signing Secret found in App Credentials.

Prepare the App on Kintone

Create the app

Create an App (External link) named "To do" with the following fields and settings.

Field Type Field Name Field Code
Text To Do name
Drop-down Status status
Date Due date due_date
User selection Assignee assignee

Generate an API Token

Generate an API Token for the App and set the View records permissions for it. For more information on API Tokens, refer to the API Tokens article.

Make note of the API token as it will be used later. Click Save and then Update App to save the changes made to the App.

Prepare the Lambda Function and API Gateway on AWS

Set up the Lambda function

Log into an AWS account, open the AWS Web Console and navigate to Lambda. Select Author from scratch and set the Function name and Runtime settings as follows. Click Create function.

Prepare the following code for the Lambda function code. Replace the following variables with the information of the Kintone and Slack environments set up earlier.

Variable Name Value
kintoneHost The Kintone domain name
appId The App ID of the To do app
slackSigningSecret The Slack Signing Secret
apiToken The Kintone API token
caution
Attention

In order to protect the token and the API token, encrypt these tokens using AWS Key Management System when in a production environment. Refer to the Creating Keys (External link) documentation of KMS for more information.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
'use strict';
const AWS = require('aws-sdk');
const qs = require('querystring');
const https = require('https');
const crypto = require('crypto');

const kintoneHost = '{subdomain}.kintone.com';
const appId = '{your app id}';

/*
  Important!
  You must define 'token' and 'apiToken' with KMS Key mechanism to make it secure.
  See http://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html
*/
const kmsEncryptedToken = process.env.kmsEncryptedToken;
const kmsEncryptedApiToken = process.env.kmsEncryptedApiToken;
let slackSigningSecret = '{your slack signing secret}';
let apiToken = '{kintone api token}';

function getOptions(path, method) {
  return {
    hostname: kintoneHost,
    port: 443,
    path: path,
    method: method,
    headers: {
      'X-Cybozu-API-Token': apiToken
    }
  };
}

function getTodos(user, callback) {
  const params = {
    'app': appId,
    'query': 'assignee in ("' + user + '") and status in ("Not started","In progress")'
  };
  const query = qs.stringify(params);
  const options = getOptions('/k/v1/records.json?' + query, 'GET');
  const req = https.request(options, function(res) {
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      if (res.statusCode === 200) {
        callback(null, JSON.parse(chunk).records);
      } else {
        callback(chunk);
      }
    });

  });

  req.on('error', function(e) {
    console.log(e);
    callback(e.message);
  });

  req.end();
}

function processEvent(event, callback) {

  const params = qs.parse(event.body);
  const requestToken = params.token;

  const headers = event.headers;
  const slackRequestTimestamp = headers['X-Slack-Request-Timestamp'];
  const slackSignature = headers['X-Slack-Signature'];
  const baseString = 'v0:' + slackRequestTimestamp + ':' + event.body;

  // hash baseString with slack_signing_secret
  const encrypt = 'v0=' + crypto.createHmac('SHA256', slackSigningSecret).update(baseString).digest('hex');

  const slackSignatureBuffer = Buffer.from(slackSignature, 'hex');
  const encryptBuffer = Buffer.from(encrypt, 'hex');

  if (!crypto.timingSafeEqual(encryptBuffer, slackSignatureBuffer)) {
    console.error('Request token' + requestToken + 'does not match expected');
    return callback('Invalid request token');
  }

  const user = params.user_name;

  getTodos(user, function(err, records) {
    if (err) {
      console.log(err);
      callback(err);
    } else {
      let msg = '';
      records.forEach(function(r) {
        if (msg) msg += '\n';
        msg += r.name.value + ' DEADLINE: ' + r.due_date.value;
      });
      callback(null, {text: msg});
    }
  });
}

function decrypt(encrypted) {
  const kms = new AWS.KMS();
  return kms.decrypt({
    CiphertextBlob: new Buffer(encrypted, 'base64')
  }).promise().then((data) => {
    return data.Plaintext.toString('ascii');
  });
}

exports.handler = (event, context, callback) => {
  const done = (err, res) => callback(null, {
    statusCode: err ? '400' : '200',
    body: err ? (err.message || err) : JSON.stringify(res),
    headers: {
      'Content-Type': 'application/json',
    },
  });
  if (slackSigningSecret && apiToken) {
    processEvent(event, done);
  } else if (kmsEncryptedToken && kmsEncryptedApiToken) {
    Promise.all([kmsEncryptedToken, kmsEncryptedApiToken].map(function(encrypted) {
      return decrypt(encrypted);
    })).then(function(params) {
      slackSigningSecret = params[0];
      apiToken = params[1];
      processEvent(event, done);
    }).catch(function(error) {
      console.log(error);
    });
  } else {
    console.log('else');
    done('Token has not been set.');
  }
};

The page should look like the following.

Set up the API Gateway

Click Add trigger to add a new Lambda trigger.

On the settings page, set the configurations as follows. Click Add once the settings are complete.

The API endpoint is displayed under the name of the Lambda function. Copy the URL to add to the Slack settings.

Test the Slash Command on Slack

On the Slack API Applications settings page, set the endpoint URL of the API Gateway that was created. Navigate to the Basic Information settings page. Under Building Apps for Slack, click Install App to Workspace.

On the next page, allow the App to access the workspace.

Navigate to the created channel within Slack. Typing in the Slash command /mytodo should run the Lambda function and the incomplete To Dos in Kintone for the current user should be displayed in the channel.

tips
Note

Note: Records are retrieved by matching the Slack login name with the Kintone login name, so only Kintone records assigned to the current logged in Slack user are displayed.

Reference

Slash Commands - Slack API (External link)

Slash Commands in Slack (External link)