Slack のチュートリアル - スラッシュ コマンド

このチュートリアルでは Cloud Functions を使用して、Google Knowledge Graph API を検索する Slack のスラッシュ コマンドの実装方法について説明します。

目標

  • Slack でスラッシュ コマンドを作成する。
  • HTTP Cloud 関数 を作成して、デプロイする。
  • スラッシュ コマンドを使用して Google Knowledge Graph API を検索する。

料金

このチュートリアルでは、以下を含む Cloud Platform の有料コンポーネントを使用します。

  • Google Cloud Functions

料金計算ツールを使用すると、予想使用量に基づいて費用の見積もりを作成できます。

始める前に

  1. Google アカウントにログインします。

    Google アカウントをまだお持ちでない場合は、新しいアカウントを登録します。

  2. GCP Console のプロジェクト セレクタのページで、GCP プロジェクトを選択または作成します。

    プロジェクト セレクタのページに移動

  3. Google Cloud Platform プロジェクトに対して課金が有効になっていることを確認します。 詳しくは、課金を有効にする方法をご覧ください。

  4. Cloud Functions および Google Knowledge Graph Search API を有効にします。

    APIを有効にする

  5. gcloud コンポーネントを更新します。
    gcloud components update
  6. 開発環境を準備します。

データの流れ

Slack のスラッシュ コマンドのチュートリアル アプリケーションでのデータの流れでは、次の手順が行われます。

  1. ユーザーが Slack チャネルで /kg <search_query> スラッシュ コマンドを実行します。
  2. Slack がコマンドのペイロードを Cloud 関数のトリガー エンドポイントに送信します。
  3. Cloud 関数がユーザーの検索クエリとともにリクエストを Knowledge Graph API に送信します。
  4. Knowledge Graph API は、レスポンスで一致した結果を返します。
  5. Cloud 関数はレスポンスを Slack メッセージの形式にフォーマット化します。
  6. Cloud 関数はメッセージを Slack に送信します。
  7. ユーザーは Slack チャンネルでフォーマット化されたレスポンスを確認します。

次の図はこの手順を可視化しています。

関数の作成

  1. ローカルマシンにサンプルアプリのリポジトリのクローンを作成します。

    Node.js

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

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

    Python

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

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

  2. Cloud Functions のサンプルコードが含まれているディレクトリに移動します。

    Node.js

    cd nodejs-docs-samples/functions/slack/

    Python

    cd python-docs-samples/functions/slack/

    Go

    cd golang-samples/functions/slack/

  3. アプリを構成します。

    Node.js

    config.default.json ファイルをテンプレートとして使用し、次のコンテンツで config.json ファイルを作成します。
    {
    "SLACK_TOKEN": "YOUR_SLACK_TOKEN",
    "KG_API_KEY": "YOUR_KG_API_KEY",
    }
    • YOUR_SLACK_TOKEN は、アプリの構成の [基本情報] ページで、Slack から提供された検証トークンに置き換えます。
    • YOUR_KG_API_KEY は、前の手順で作成した Knowledge Graph API Key に置き換えます。

    Python

    次のコンテンツが含まれるように config.json ファイルを編集します。
    {
    "SLACK_TOKEN": "YOUR_SLACK_TOKEN",
    "KG_API_KEY": "YOUR_KG_API_KEY",
    }
    • YOUR_SLACK_TOKEN は、アプリの構成の [基本情報] ページで、Slack から提供された検証トークンに置き換えます。
    • YOUR_KG_API_KEY は、前の手順で作成した Knowledge Graph API Key に置き換えます。

    Go

    次のコンテンツが含まれるように config.json ファイルを編集します。
    {
    "SLACK_TOKEN": "YOUR_SLACK_TOKEN",
    "KG_API_KEY": "YOUR_KG_API_KEY",
    }
    • YOUR_SLACK_TOKEN は、アプリの構成の [基本情報] ページで、Slack から提供された検証トークンに置き換えます。
    • YOUR_KG_API_KEY は、前の手順で作成した Knowledge Graph API Key に置き換えます。

関数のデプロイ

ユーザー(または Slack)が関数のエンドポイントに HTTP POST リクエストを送信したときに実行される関数をデプロイするには、Cloud Functions サンプルコードが格納されたディレクトリで次のコマンドを実行します。

Node.js

gcloud functions deploy kgSearch --runtime nodejs8 --trigger-http
異なるバージョンの Node.js を使用するには、--runtime フラグに次の値を使用します。
  • nodejs6(非推奨)
  • nodejs8
  • nodejs10(ベータ版)

Python

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

Go

gcloud functions deploy KGSearch --runtime go111 --trigger-http

アプリケーションの構成

関数のデプロイ後は、Slack のスラッシュ コマンドを作成する必要があります。このコマンドがトリガーされるたびに Cloud 関数にクエリが送信されます。

  1. Slack のスラッシュ コマンドをホストする Slack アプリを作成します。 統合のインストール権限が与えられている Slack チームにそれを関連付けます。

  2. [Slash commands] に移動し、[Create new command] ボタンをクリックします。

  3. コマンドの名前として「/kg」を入力します。

  4. コマンドの URL を入力します。

    Node.js

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

    Python

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

    Go

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

    YOUR_REGION は、Cloud 関数がデプロイされているリージョンであり、YOUR_PROJECT_ID は Cloud プロジェクト ID です。

    関数のデプロイが完了すると、両方の値がターミナルに表示されます。

  5. [保存] をクリックします。

  6. [Basic Information] に移動します。

  7. [Install your app to your workspace] をクリックし、画面の指示に従って、ワークスペース用にアプリケーションを有効にします。

    少し待つと、Slack のスラッシュ コマンドがオンラインになります。

コードの説明

依存関係をインポートする

アプリケーションが Google Cloud Platform サービスとやり取りするには、いくつかの依存関係をインポートする必要があります。

Node.js

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

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

Python

import json
import os

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=os.environ['API_KEY'] or config['KG_API_KEY'])

Go


package slack

import (
	"context"
	"encoding/json"
	"log"
	"os"

	"google.golang.org/api/kgsearch/v1"
	"google.golang.org/api/option"
)

type configuration struct {
	ProjectID string `json:"PROJECT_ID"`
	Token     string `json:"SLACK_TOKEN"`
	Key       string `json:"KG_API_KEY"`
}

var (
	entitiesService *kgsearch.EntitiesService
	config          *configuration
)

func setup(ctx context.Context) {
	if config == nil {
		cfgFile, err := os.Open("config.json")
		if err != nil {
			log.Fatalf("os.Open: %v", err)
		}

		d := json.NewDecoder(cfgFile)
		config = &configuration{}
		if err = d.Decode(config); err != nil {
			log.Fatalf("Decode: %v", err)
		}
	}

	if entitiesService == nil {
		kgService, err := kgsearch.NewService(ctx, option.WithAPIKey(config.Key))
		if err != nil {
			log.Fatalf("kgsearch.NewClient: %v", err)
		}
		entitiesService = kgsearch.NewEntitiesService(kgService)
	}
}

Webhook の受け取り

ユーザー(または Slack)が関数のエンドポイントに HTTP POST リクエストを送信すると、次の関数が実行されます。

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 = async (req, res) => {
  try {
    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
    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);
  }
};

Python

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)

Go


// Package slack is a Cloud Function which recieves a query from
// a Slack command and responds with the KG API result.
package slack

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
)

type attachment struct {
	Color     string `json:"color"`
	Title     string `json:"title"`
	TitleLink string `json:"title_link"`
	Text      string `json:"text"`
	ImageURL  string `json:"image_url"`
}

// Message is the a Slack message event.
// see https://api.slack.com/docs/message-formatting
type Message struct {
	ResponseType string       `json:"response_type"`
	Text         string       `json:"text"`
	Attachments  []attachment `json:"attachments"`
}

// KGSearch uses the Knowledge Graph API to search for a query provided
// by a Slack command.
func KGSearch(w http.ResponseWriter, r *http.Request) {
	setup(r.Context())
	if r.Method != "POST" {
		http.Error(w, "Only POST requests are accepted", 405)
	}
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Couldn't parse form", 400)
		log.Fatalf("ParseForm: %v", err)
	}
	if err := verifyWebHook(r.Form); err != nil {
		log.Fatalf("verifyWebhook: %v", err)
	}
	if len(r.Form["text"]) == 0 {
		log.Fatalf("emtpy text in form")
	}
	kgSearchResponse, err := makeSearchRequest(r.Form["text"][0])
	if err != nil {
		log.Fatalf("makeSearchRequest: %v", err)
	}
	w.Header().Set("Content-Type", "application/json")
	if err = json.NewEncoder(w).Encode(kgSearchResponse); err != nil {
		log.Fatalf("json.Marshal: %v", err)
	}
}

次の関数は、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.
 */
const verifyWebhook = body => {
  if (!body || body.token !== config.SLACK_TOKEN) {
    const error = new Error('Invalid credentials');
    error.code = 401;
    throw error;
  }
};

Python

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

Go

func verifyWebHook(form url.Values) error {
	t := form.Get("token")
	if len(t) == 0 {
		return fmt.Errorf("empty form token")
	}
	if t != config.Token {
		return fmt.Errorf("invalid request/credentials: %q", t[0])
	}
	return nil
}

Knowledge Graph API のクエリ作成

次の関数はユーザーの検索クエリとともにリクエストを Knowledge Graph API に送信します。

Node.js

/**
 * 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: 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

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

Go

func makeSearchRequest(query string) (*Message, error) {
	res, err := entitiesService.Search().Query(query).Limit(1).Do()
	if err != nil {
		return nil, fmt.Errorf("Do: %v", err)
	}
	return formatSlackMessage(query, res)
}

Slack メッセージのフォーマット化

最後に、次の関数は Knowledge Graph の結果をフォーマット化し、ユーザーに表示される適切な形式の Slack メッセージにします。

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.
 */
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;
};

Python

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

Go


package slack

import (
	"fmt"

	"google.golang.org/api/kgsearch/v1"
)

func formatSlackMessage(query string, response *kgsearch.SearchResponse) (*Message, error) {
	if response == nil {
		return nil, fmt.Errorf("empty response")
	}

	if response.ItemListElement == nil || len(response.ItemListElement) == 0 {
		message := &Message{
			ResponseType: "in_channel",
			Text:         fmt.Sprintf("Query: %s", query),
			Attachments: []attachment{
				{
					Color: "#d6334b",
					Text:  "No results match your query.",
				},
			},
		}
		return message, nil
	}

	entity, ok := response.ItemListElement[0].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("could not parse response entity")
	}
	result, ok := entity["result"].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("error formatting response result")
	}

	attach := attachment{Color: "#3367d6"}
	if name, ok := result["name"].(string); ok {
		if description, ok := result["description"].(string); ok {
			attach.Title = fmt.Sprintf("%s: %s", name, description)
		} else {
			attach.Title = name
		}
	}
	if detailedDesc, ok := result["detailedDescription"].(map[string]interface{}); ok {
		if url, ok := detailedDesc["url"].(string); ok {
			attach.TitleLink = url
		}
		if article, ok := detailedDesc["articleBody"].(string); ok {
			attach.Text = article
		}
	}
	if image, ok := result["image"].(map[string]interface{}); ok {
		if imageURL, ok := image["contentUrl"].(string); ok {
			attach.ImageURL = imageURL
		}
	}

	message := &Message{
		ResponseType: "in_channel",
		Text:         fmt.Sprintf("Query: %s", query),
		Attachments:  []attachment{attach},
	}
	return message, nil
}

Slack API のタイムアウト

Slack API では、関数が Webhook リクエストを受け取ってから 3 秒以内に応答することが想定されています。

通常、このチュートリアルのコマンドは 3 秒以内に応答します。実行時間が長いコマンドの場合、タスクキューとして機能する Pub/Sub トピックに(リクエストの response_url を含む)リクエストを push する関数を構成することをおすすめします。

この構成によって、2 番目の関数を作成して Pub/Sub によってトリガーすることで、それらのタスクを処理し、Slack の response_url に結果を送信できます。

スラッシュ コマンドの使用

  1. 手動でコマンドをテストします。

    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

    curl -X POST "https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/kg_search" --data 'token=YOUR_SLACK_TOKEN&text=giraffe'

    Go

    curl -X POST "https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/KGSearch" --data 'token=YOUR_SLACK_TOKEN&text=giraffe'

    ここで

    • YOUR_REGION は関数がデプロイされるリージョンです。関数のデプロイが完了すると、これがターミナルに表示されます。
    • YOUR_PROJECT_ID は Cloud プロジェクト ID です。関数のデプロイが完了すると、これがターミナルに表示されます。
    • YOUR_SLACK_TOKEN はスラッシュ コマンドの構成で Slack から提供されたトークンです。
  2. 実行した内容が完了していることをログで確認します。

    gcloud functions logs read --limit 100
    
  3. Slack チャンネルにコマンドを入力します。

    /kg giraffe

クリーンアップ

このチュートリアルで使用するリソースについて、Google Cloud Platform アカウントへの課金が発生しないようにする手順は次のとおりです。

プロジェクトの削除

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

  1. GCP Console で [プロジェクト] ページに移動します。

    プロジェクト ページに移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

Cloud 関数の削除

このチュートリアルでデプロイした Cloud 関数を削除するには、次のコマンドを実行します。

Node.js

gcloud functions delete kgSearch 

Python

gcloud functions delete kg_search 

Go

gcloud functions delete KGSearch 

Google Cloud Console から Cloud Functions を削除することもできます。

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...