Tutorial do Slack: comandos do Slash

Neste tutorial, você aprenderá como usar o Cloud Functions para implementar um Slack Slash Command que pesquisa a API Google Knowledge Graph.

Objetivos

  • Criar um Slash Command no Slack.
  • Escrever e implantar um Cloud Function HTTP.
  • Pesquisar a API Google Knowledge Graph usando o Slash Command.

Custos

Este tutorial usa componentes do Cloud Platform que podem ser cobrados, incluindo:

  • Google Cloud Functions

Use a calculadora de preços para gerar uma estimativa de custo com base no uso previsto.

Antes de começar

  1. Faça login na sua Conta do Google.

    Se você ainda não tiver uma, inscreva-se.

  2. No Console do GCP, na página do seletor de projetos, selecione ou crie um projeto do GCP.

    Acesse a página do seletor de projetos

  3. Verifique se o faturamento foi ativado no projeto do Google Cloud Platform. Saiba como confirmar que o faturamento está ativado para seu projeto.

  4. Ative a(s) Cloud Functions e Google Knowledge Graph Search APIs necessária(s).

    Ativar a(s) APIs

  5. Atualize os componentes gcloud:
    gcloud components update
  6. Prepare seu ambiente de desenvolvimento.

Como visualizar o fluxo de dados

O fluxo de dados no aplicativo de tutorial do Slack Slash Command envolve vários passos:

  1. O usuário executa o Slash Command /kg <search_query> em um canal Slack.
  2. O Slack envia o payload do comando ao endpoint do acionador do Cloud Function.
  3. O Cloud Function envia uma solicitação com a consulta de pesquisa do usuário para a API Knowledge Graph.
  4. A API Knowledge Graph responde com todos os resultados correspondentes.
  5. O Cloud Function formata a resposta em uma mensagem do Slack.
  6. O Cloud Function envia a mensagem de volta ao Slack.
  7. O usuário vê a resposta formatada no canal do Slack.

Isso pode ajudar a visualizar os passos:

Como preparar a função

  1. Clone o repositório do app de amostra na máquina local:

    Node.js

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

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

    Python

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

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

    Go

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

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

  2. Altere para o diretório que contém o código de amostra do Cloud Functions:

    Node.js

    cd nodejs-docs-samples/functions/slack/

    Python

    cd python-docs-samples/functions/slack/

    Go

    cd golang-samples/functions/slack/

  3. Configure o aplicativo:

    Node.js

    Usando o arquivo config.default.json como modelo, crie um arquivo config.json com o seguinte conteúdo:
    {
    "SLACK_TOKEN": "YOUR_SLACK_TOKEN",
    "KG_API_KEY": "YOUR_KG_API_KEY",
    }
    • Substitua YOUR_SLACK_TOKEN pelo token de verificação fornecido pelo Slack na página Informações básicas da configuração do aplicativo.
    • Substitua YOUR_KG_API_KEY pela chave da API Graph do Knowledge que você acabou de criar.

    Python

    Edite o arquivo config.json para ter o seguinte conteúdo:
    {
    "SLACK_TOKEN": "YOUR_SLACK_TOKEN",
    "KG_API_KEY": "YOUR_KG_API_KEY",
    }
    • Substitua YOUR_SLACK_TOKEN pelo token de verificação fornecido pelo Slack na página Informações básicas da configuração do aplicativo.
    • Substitua YOUR_KG_API_KEY pela chave da API Graph do Knowledge que você acabou de criar.

    Go

    Edite o arquivo config.json para ter o seguinte conteúdo:
    {
    "SLACK_TOKEN": "YOUR_SLACK_TOKEN",
    "KG_API_KEY": "YOUR_KG_API_KEY",
    }
    • Substitua YOUR_SLACK_TOKEN pelo token de verificação fornecido pelo Slack na página Informações básicas da configuração do aplicativo.
    • Substitua YOUR_KG_API_KEY pela chave da API Graph do Knowledge que você acabou de criar.

Como implantar a função

Para implantar a função que é executada quando você ou o Slack fazem uma solicitação HTTP POST para o endpoint da função, execute o seguinte comando no diretório que contém o código de amostra Cloud Functions:

Node.js

gcloud functions deploy kgSearch --runtime nodejs8 --trigger-http
É possível usar os seguintes valores para a sinalização --runtime para usar versões diferentes do Node.js:
  • nodejs6 (uso suspenso)
  • nodejs8
  • nodejs10 (Beta)

Python

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

Go

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

Como configurar o aplicativo

Depois que a função é implementada, você precisa criar um Slack Slash Command que envia a consulta para o Cloud Function toda vez que o comando for acionado:

  1. Crie um aplicativo Slack para hospedar o Slack Slash Command. Associe-o a uma equipe do Slack onde você tem permissões para instalar integrações.

  2. Vá para os comandos do Slash e clique no botão Criar novo comando.

  3. Insira /kg como o nome do comando.

  4. Digite o URL do comando:

    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

    em que YOUR_REGION é a região em que o Cloud Function está implantado e YOUR_PROJECT_ID é seu ID do projeto do Cloud.

    Ambos os valores são visíveis em seu terminal quando sua função termina de ser implantada.

  5. Clique em Salvar.

  6. Vá para Informações básicas.

  7. Clique em Instalar aplicativo no espaço de trabalho e siga as instruções na tela para ativar o aplicativo do seu espaço de trabalho.

    Seu Slack Slash Command ficará on-line em breve.

Como entender o código

Como importar dependências

O aplicativo importa várias dependências para se comunicar com os serviços do 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)
	}
}

Como receber o webhook

A função a seguir é executada quando você ou o Slack fazem uma solicitação HTTP POST para o endpoint da função:

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)
	}
}

A função a seguir autentica a solicitação de entrada, verificando o token gerado pelo 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
}

Como consultar a API Knowledge Graph

A função a seguir envia uma solicitação com a consulta de pesquisa do usuário para a API Knowledge Graph:

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)
}

Como formatar a mensagem do Slack

Por fim, a seguinte função exibe para o usuário o resultado do Knowledge Graph em uma mensagem do Slack ricamente formatada:

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
}

Tempo limite da API do Slack

A API do Slack espera que sua função responda dentro de 3 segundos após receber uma solicitação de webhook.

Os comandos deste tutorial normalmente levam menos de três segundos para responder. Para comando de execução mais longa, recomendamos configurar uma função para pedidos push (incluindo seu response_url) para um tópico do Pub/Sub que atua como uma fila de tarefas.

Em seguida, crie uma segunda função acionada por Pub/Sub que processa essas tarefas e envia os resultados de volta para o response_url do Slack.

Como usar o comando Slash

  1. Teste o comando manualmente:

    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'

    em que

    • YOUR_REGION é a região onde sua função é implantada. Visível no seu terminal quando a implantação da função termina.
    • YOUR_PROJECT_ID é seu ID do projeto do Cloud. Visível no seu terminal quando a implantação da função termina.
    • YOUR_SLACK_TOKEN é o token fornecido pelo Slack na sua configuração do Slash Command.
  2. Verifique os registros para ter certeza de que as execuções foram concluídas:

    gcloud functions logs read --limit 100
    
  3. Digite o comando no seu canal do Slack:

    /kg giraffe

Limpeza

Para evitar que os recursos usados neste tutorial sejam cobrados na conta do Google Cloud Platform:

Como excluir o projeto

O jeito mais fácil de evitar cobranças é excluindo o projeto que você criou para o tutorial.

Para excluir o projeto:

  1. No Console do GCP, acesse a página Gerenciar recursos.

    Acessar a página Gerenciar recursos

  2. Na lista de projetos, selecione o projeto que você quer excluir e clique em Excluir .
  3. Na caixa de diálogo, digite o ID do projeto e clique em Encerrar para excluí-lo.

Como excluir o Cloud Function

Para excluir o Cloud Function implantado neste tutorial, execute o seguinte comando:

Node.js

gcloud functions delete kgSearch 

Python

gcloud functions delete kg_search 

Go

gcloud functions delete KGSearch 

Também é possível excluir o Cloud Functions do Console do Google Cloud.

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…