functions/slack/index.js (99 lines of code) (raw):
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
// [START functions_slack_setup]
const functions = require('@google-cloud/functions-framework');
const google = require('@googleapis/kgsearch');
const {verifyRequestSignature} = require('@slack/events-api');
// Get a reference to the Knowledge Graph Search component
const kgsearch = google.kgsearch('v1');
// [END functions_slack_setup]
// [START functions_slack_format]
/**
* 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.
*/
const formatSlackMessage = (query, response) => {
let entity;
// Extract the first entity from the result list, if any
if (
response &&
response.data &&
response.data.itemListElement &&
response.data.itemListElement.length > 0
) {
entity = response.data.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;
};
// [END functions_slack_format]
// [START functions_verify_webhook]
/**
* Verify that the webhook request came from Slack.
*
* @param {object} req Cloud Function request object.
* @param {string} req.headers Headers Slack SDK uses to authenticate request.
* @param {string} req.rawBody Raw body of webhook request to check signature against.
*/
const verifyWebhook = req => {
const signature = {
signingSecret: process.env.SLACK_SECRET,
requestSignature: req.headers['x-slack-signature'],
requestTimestamp: req.headers['x-slack-request-timestamp'],
body: req.rawBody,
};
// This method throws an exception if an incoming request is invalid.
verifyRequestSignature(signature);
};
// [END functions_verify_webhook]
// [START functions_slack_request]
/**
* Send the user's search query to the Knowledge Graph API.
*
* @param {string} query The user's search query.
*/
const makeSearchRequest = query => {
return new Promise((resolve, reject) => {
kgsearch.entities.search(
{
auth: process.env.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));
}
);
});
};
// [END functions_slack_request]
// [START functions_slack_search]
/**
* Receive a Slash Command request from Slack.
*
* Trigger this function by creating a Slack slash command with the HTTP Trigger URL.
* You can find the HTTP URL in the Cloud Console or using `gcloud functions describe`
*
* @param {object} req Cloud Function request object.
* @param {object} req.body The request payload.
* @param {string} req.rawBody Raw request payload used to validate Slack's message signature.
* @param {string} req.body.text The user's search query.
* @param {object} res Cloud Function response object.
*/
functions.http('kgSearch', async (req, res) => {
try {
if (req.method !== 'POST') {
const error = new Error('Only POST requests are accepted');
error.code = 405;
throw error;
}
if (!req.body.text) {
const error = new Error('No text found in body.');
error.code = 400;
throw error;
}
// Verify that this request came from Slack
verifyWebhook(req);
// Make the request to the Knowledge Graph Search API
const response = await makeSearchRequest(req.body.text);
// Send the formatted message back to Slack
res.json(response);
return Promise.resolve();
} catch (err) {
console.error(err);
res.status(err.code || 500).send(err);
return Promise.reject(err);
}
});
// [END functions_slack_search]